diff options
Diffstat (limited to 'lib/vendors/service.ts')
| -rw-r--r-- | lib/vendors/service.ts | 409 |
1 files changed, 407 insertions, 2 deletions
diff --git a/lib/vendors/service.ts b/lib/vendors/service.ts index 16f57b57..fb834814 100644 --- a/lib/vendors/service.ts +++ b/lib/vendors/service.ts @@ -2,8 +2,9 @@ import { revalidateTag, unstable_noStore } from "next/cache"; import db from "@/db/db"; -import { vendorAttachments, VendorContact, vendorContacts, vendorDetailView, vendorItemsView, vendorPossibleItems, vendors, vendorsWithTypesView, vendorTypes, type Vendor } from "@/db/schema"; +import { vendorAttachments, VendorContact, vendorContacts, vendorDetailView, vendorItemsView, vendorMaterialsView, vendorPossibleItems, vendorPossibleMateirals, vendors, vendorsWithTypesView, vendorTypes, type Vendor } from "@/db/schema"; import logger from '@/lib/logger'; +import * as z from "zod" import { filterColumns } from "@/lib/filter-columns"; import { unstable_cache } from "@/lib/unstable-cache"; @@ -28,6 +29,8 @@ import { selectRfqHistory, selectVendorsWithTypes, countVendorsWithTypes, + countVendorMaterials, + insertVendorMaterial, } from "./repository"; @@ -40,6 +43,7 @@ import type { GetVendorItemsSchema, CreateVendorItemSchema, GetRfqHistorySchema, + GetVendorMaterialsSchema, } from "./validations"; import { asc, desc, ilike, inArray, and, or, gte, lte, eq, isNull, count, sql } from "drizzle-orm"; @@ -51,7 +55,7 @@ import JSZip from 'jszip'; import { promises as fsPromises } from 'fs'; import { sendEmail } from "../mail/sendEmail"; import { PgTransaction } from "drizzle-orm/pg-core"; -import { items } from "@/db/schema/items"; +import { items, materials } from "@/db/schema/items"; import { users } from "@/db/schema/users"; import { getServerSession } from "next-auth"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; @@ -740,6 +744,86 @@ export async function getVendorItems(input: GetVendorItemsSchema, id: number) { return cachedFunction(); } +export async function getVendorMaterials(input: GetVendorMaterialsSchema, id: number) { + const cachedFunction = 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: vendorMaterialsView, + filters: input.filters, + joinOperator: input.joinOperator, + }); + + + let globalWhere + if (input.search) { + const s = `%${input.search}%` + globalWhere = or(ilike(vendorMaterialsView.itemCode, s) + , ilike(vendorMaterialsView.description, s) + ) + // 필요시 여러 칼럼 OR조건 (status, priority, etc) + } + + const vendorWhere = eq(vendorMaterialsView.vendorId, id) + + const finalWhere = and( + // advancedWhere or your existing conditions + advancedWhere, + globalWhere, + vendorWhere + ) + + + // 아니면 ilike, inArray, gte 등으로 where 절 구성 + const where = finalWhere + + const orderBy = + input.sort.length > 0 + ? input.sort.map((item) => + item.desc ? desc(vendorMaterialsView[item.id]) : asc(vendorMaterialsView[item.id]) + ) + : [asc(vendorMaterialsView.createdAt)]; + + // 트랜잭션 내부에서 Repository 호출 + const { data, total } = await db.transaction(async (tx) => { + const data = await selectVendorMaterials(tx, { + where, + orderBy, + offset, + limit: input.perPage, + }); + const total = await countVendorMaterials(tx, where); + return { data, total }; + }); + + + const pageCount = Math.ceil(total / input.perPage); + + + console.log(data) + + return { data, pageCount }; + } catch (err) { + // 에러 발생 시 디폴트 + return { data: [], pageCount: 0 }; + } + }, + [JSON.stringify(input), String(id)], // 캐싱 키 + { + revalidate: 3600, + tags: [`vendor-materials-${id}`], // revalidateTag("tasks") 호출 시 무효화 + } + ); + return cachedFunction(); +} + export interface ItemDropdownOption { itemCode: string; itemName: string; @@ -820,6 +904,327 @@ export async function createVendorItem(input: CreateVendorItemSchema) { } } + + +const updateVendorItemSchema = z.object({ + oldItemCode: z.string().min(1, "Old item code is required"), + newItemCode: z.string().min(1, "New item code is required"), + vendorId: z.number().min(1, "Vendor ID is required"), +}) + + +export async function deleteVendorItem( + vendorId: number, + itemCode: string +) { + try { + const validatedData = deleteVendorItemSchema.parse({ + itemCode, + vendorId, + }) + + await db + .delete(vendorPossibleItems) + .where( + and( + eq(vendorPossibleItems.itemCode, validatedData.itemCode), + eq(vendorPossibleItems.vendorId, validatedData.vendorId) + ) + ) + + revalidateTag(`vendor-items-${vendorId}`); + + return { success: true, message: "Item deleted successfully" } + } catch (error) { + console.error("Error deleting vendor item:", error) + return { + success: false, + message: error instanceof z.ZodError + ? error.errors[0].message + : "Failed to delete item" + } + } +} + +export async function updateVendorItem( + vendorId: number, + oldItemCode: string, + newItemCode: string +) { + unstable_noStore(); // Next.js 서버 액션 캐싱 방지 + + try { + const validatedData = updateVendorItemSchema.parse({ + oldItemCode, + newItemCode, + vendorId, + }) + + await db.transaction(async (tx) => { + // 기존 아이템 삭제 + await tx + .delete(vendorPossibleItems) + .where( + and( + eq(vendorPossibleItems.itemCode, validatedData.oldItemCode), + eq(vendorPossibleItems.vendorId, validatedData.vendorId) + ) + ) + + // 새 아이템 추가 + await tx.insert(vendorPossibleItems).values({ + vendorId: validatedData.vendorId, + itemCode: validatedData.newItemCode, + }) + }) + + // 캐시 무효화 + revalidateTag(`vendor-items-${vendorId}`) + + return { data: null, error: null } + } catch (err) { + console.error("Error updating vendor item:", err) + return { + data: null, + error: getErrorMessage(err) + } + } +} + +export async function removeVendorItems(input: { + itemCodes: string[] + vendorId: number +}) { + unstable_noStore() + + try { + const validatedData = removeVendorItemsSchema.parse(input) + + await db + .delete(vendorPossibleItems) + .where( + and( + inArray(vendorPossibleItems.itemCode, validatedData.itemCodes), + eq(vendorPossibleItems.vendorId, validatedData.vendorId) + ) + ) + + revalidateTag(`vendor-items-${validatedData.vendorId}`) + + return { data: null, error: null } + } catch (err) { + console.error("Error deleting vendor items:", err) + return { + data: null, + error: getErrorMessage(err) + } + } +} + +// 스키마도 추가해야 합니다 +const removeVendorItemsSchema = z.object({ + itemCodes: z.array(z.string()).min(1, "At least one item code is required"), + vendorId: z.number().min(1, "Vendor ID is required"), +}) + +const deleteVendorItemSchema = z.object({ + itemCode: z.string().min(1, "Item code is required"), + vendorId: z.number().min(1, "Vendor ID is required"), +}) + +export async function getMaterialsForVendor(vendorId: number) { + return unstable_cache( + async () => { + try { + // 해당 vendorId가 이미 가지고 있는 itemCode 목록을 서브쿼리로 구함 + // 그 아이템코드를 제외(notIn)하여 모든 items 테이블에서 조회 + const itemsData = await db + .select({ + itemCode: materials.itemCode, + itemName: materials.itemName, + description: materials.description, + }) + .from(materials) + .leftJoin( + vendorPossibleMateirals, + eq(materials.itemCode, vendorPossibleMateirals.itemCode) + ) + // vendorPossibleItems.vendorId가 이 vendorId인 행이 없는(즉 아직 등록되지 않은) 아이템만 + .where( + isNull(vendorPossibleMateirals.id) // 또는 isNull(vendorPossibleItems.itemCode) + ) + .orderBy(asc(materials.itemName)) + + return { + data: itemsData.map((item) => ({ + itemCode: item.itemCode ?? "", // null이라면 ""로 치환 + itemName: item.itemName, + description: item.description ?? "" // null이라면 ""로 치환 + })), + error: null + } + } catch (err) { + console.error("Failed to fetch items for vendor dropdown:", err) + return { + data: [], + error: "아이템 목록을 불러오는데 실패했습니다.", + } + } + }, + // 캐시 키를 vendorId 별로 달리 해야 한다. + ["materials-for-vendor", String(vendorId)], + { + revalidate: 3600, // 1시간 캐싱 + tags: ["materials"], // revalidateTag("materials") 호출 시 무효화 + } + )() +} + +export async function createVendorMaterial(input: CreateVendorItemSchema) { + unstable_noStore(); // Next.js 서버 액션 캐싱 방지 + try { + await db.transaction(async (tx) => { + // DB Insert + const [newContact] = await insertVendorMaterial(tx, { + vendorId: input.vendorId, + itemCode: input.itemCode, + + }); + return newContact; + }); + + // 캐시 무효화 (협력업체 연락처 목록 등) + revalidateTag(`vendor-materials-${input.vendorId}`); + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +const updateVendorMaterialSchema = z.object({ + oldItemCode: z.string().min(1, "Old item code is required"), + newItemCode: z.string().min(1, "New item code is required"), + vendorId: z.number().min(1, "Vendor ID is required"), +}) + + +export async function deleteVendorMaterial( + vendorId: number, + itemCode: string +) { + try { + const validatedData = deleteVendorItemSchema.parse({ + itemCode, + vendorId, + }) + + await db + .delete(vendorPossibleMateirals) + .where( + and( + eq(vendorPossibleMateirals.itemCode, validatedData.itemCode), + eq(vendorPossibleMateirals.vendorId, validatedData.vendorId) + ) + ) + + revalidateTag(`vendor-materials-${vendorId}`); + + return { success: true, message: "Item deleted successfully" } + } catch (error) { + console.error("Error deleting vendor item:", error) + return { + success: false, + message: error instanceof z.ZodError + ? error.errors[0].message + : "Failed to delete item" + } + } +} + +export async function updateVendorMaterial( + vendorId: number, + oldItemCode: string, + newItemCode: string +) { + unstable_noStore(); // Next.js 서버 액션 캐싱 방지 + + try { + const validatedData = updateVendorMaterialSchema.parse({ + oldItemCode, + newItemCode, + vendorId, + }) + + await db.transaction(async (tx) => { + // 기존 아이템 삭제 + await tx + .delete(vendorPossibleMateirals) + .where( + and( + eq(vendorPossibleMateirals.itemCode, validatedData.oldItemCode), + eq(vendorPossibleMateirals.vendorId, validatedData.vendorId) + ) + ) + + // 새 아이템 추가 + await tx.insert(vendorPossibleMateirals).values({ + vendorId: validatedData.vendorId, + itemCode: validatedData.newItemCode, + }) + }) + + // 캐시 무효화 + revalidateTag(`vendor-items-${vendorId}`) + + return { data: null, error: null } + } catch (err) { + console.error("Error updating vendor item:", err) + return { + data: null, + error: getErrorMessage(err) + } + } +} + +export async function removeVendorMaterials(input: { + itemCodes: string[] + vendorId: number +}) { + unstable_noStore() + + try { + const validatedData = removeVendormaterialsSchema.parse(input) + + await db + .delete(vendorPossibleMateirals) + .where( + and( + inArray(vendorPossibleMateirals.itemCode, validatedData.itemCodes), + eq(vendorPossibleMateirals.vendorId, validatedData.vendorId) + ) + ) + + revalidateTag(`vendor-materials-${validatedData.vendorId}`) + + return { data: null, error: null } + } catch (err) { + console.error("Error deleting vendor items:", err) + return { + data: null, + error: getErrorMessage(err) + } + } +} + +// 스키마도 추가해야 합니다 +const removeVendormaterialsSchema = z.object({ + itemCodes: z.array(z.string()).min(1, "At least one item code is required"), + vendorId: z.number().min(1, "Vendor ID is required"), +}) + + + export async function getRfqHistory(input: GetRfqHistorySchema, vendorId: number) { return unstable_cache( async () => { |
