summaryrefslogtreecommitdiff
path: root/lib/vendors/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendors/service.ts')
-rw-r--r--lib/vendors/service.ts409
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 () => {