From 44b74ff4170090673b6eeacd8c528e0abf47b7aa Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 1 Dec 2025 19:52:06 +0900 Subject: (김준회) deprecated code 정리 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/procurement-rfqs/services.ts | 2050 -------------------------------------- 1 file changed, 2050 deletions(-) delete mode 100644 lib/procurement-rfqs/services.ts (limited to 'lib/procurement-rfqs/services.ts') diff --git a/lib/procurement-rfqs/services.ts b/lib/procurement-rfqs/services.ts deleted file mode 100644 index 9cca4c73..00000000 --- a/lib/procurement-rfqs/services.ts +++ /dev/null @@ -1,2050 +0,0 @@ -"use server"; // Next.js 서버 액션에서 직접 import하려면 (선택) - -import { revalidatePath, revalidateTag, unstable_noStore } from "next/cache"; -import db from "@/db/db"; - -import { filterColumns } from "@/lib/filter-columns"; -import { unstable_cache } from "@/lib/unstable-cache"; -import { getErrorMessage } from "@/lib/handle-error"; -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" - -import { GetPORfqsSchema, GetQuotationsSchema } from "./validations"; -import { asc, desc, ilike, inArray, and, gte, lte, not, or, sql, eq, isNull, ne, isNotNull, count, between } from "drizzle-orm"; -import { incoterms, paymentTerms, prItems, prItemsView, procurementAttachments, procurementQuotationItems, procurementRfqComments, procurementRfqDetails, procurementRfqDetailsView, procurementRfqs, procurementRfqsView, procurementVendorQuotations } from "@/db/schema/procurementRFQ"; -import { countPORfqs, selectPORfqs } from "./repository"; -import { writeFile, mkdir } from "fs/promises" -import { join } from "path" -import { v4 as uuidv4 } from "uuid" -import { items, projects, users, vendors } from "@/db/schema"; -import { formatISO } from "date-fns"; -import { sendEmail } from "../mail/sendEmail"; -import { formatDate } from "../utils"; - -async function getAuthenticatedUser() { - const session = await getServerSession(authOptions); - - if (!session || !session.user?.id) { - throw new Error("인증이 필요합니다"); - } - - return { - userId: session.user.id, - user: session.user - }; -} - - -export async function getPORfqs(input: GetPORfqsSchema) { - return unstable_cache( - async () => { - try { - const offset = (input.page - 1) * input.perPage; - - // 기본 필터 처리 - RFQFilterBox에서 오는 필터 - const basicFilters = input.basicFilters || []; - const basicJoinOperator = input.basicJoinOperator || "and"; - - // 고급 필터 처리 - 테이블의 DataTableFilterList에서 오는 필터 - const advancedFilters = input.filters || []; - const advancedJoinOperator = input.joinOperator || "and"; - - // 기본 필터 조건 생성 - let basicWhere; - if (basicFilters.length > 0) { - basicWhere = filterColumns({ - table: procurementRfqsView, - filters: basicFilters, - joinOperator: basicJoinOperator, - }); - } - - // 고급 필터 조건 생성 - let advancedWhere; - if (advancedFilters.length > 0) { - advancedWhere = filterColumns({ - table: procurementRfqsView, - filters: advancedFilters, - joinOperator: advancedJoinOperator, - }); - } - - // 전역 검색 조건 - let globalWhere; - if (input.search) { - const s = `%${input.search}%`; - globalWhere = or( - ilike(procurementRfqsView.rfqCode, s), - ilike(procurementRfqsView.projectCode, s), - ilike(procurementRfqsView.projectName, s), - ilike(procurementRfqsView.dueDate, s), - ilike(procurementRfqsView.status, s), - // 발주담당 검색 추가 - ilike(procurementRfqsView.picCode, s) - ); - } - - // 날짜 범위 필터링을 위한 특별 처리 (RFQFilterBox는 이미 basicFilters에 포함) - // 이 코드는 기존 처리와의 호환성을 위해 유지 - let dateRangeWhere; - if (input.filters) { - const rfqSendDateFilter = input.filters.find(f => f.id === "rfqSendDate" && Array.isArray(f.value)); - - if (rfqSendDateFilter && Array.isArray(rfqSendDateFilter.value)) { - const [fromDate, toDate] = rfqSendDateFilter.value; - - if (fromDate && toDate) { - // 시작일과 종료일이 모두 있는 경우 - dateRangeWhere = between( - procurementRfqsView.rfqSendDate, - new Date(fromDate), - new Date(toDate) - ); - } else if (fromDate) { - // 시작일만 있는 경우 - dateRangeWhere = sql`${procurementRfqsView.rfqSendDate} >= ${new Date(fromDate)}`; - } - } - } - - // 모든 조건 결합 - let whereConditions = []; - if (basicWhere) whereConditions.push(basicWhere); - if (advancedWhere) whereConditions.push(advancedWhere); - if (globalWhere) whereConditions.push(globalWhere); - if (dateRangeWhere) whereConditions.push(dateRangeWhere); - - // 조건이 있을 때만 and() 사용 - const finalWhere = whereConditions.length > 0 - ? and(...whereConditions) - : undefined; - - - - // 정렬 조건 - 안전하게 처리 - const orderBy = - input.sort && input.sort.length > 0 - ? input.sort.map((item) => - item.desc - ? desc(procurementRfqsView[item.id]) - : asc(procurementRfqsView[item.id]) - ) - : [desc(procurementRfqsView.updatedAt)] - - - // 트랜잭션 내부에서 Repository 호출 - const { data, total } = await db.transaction(async (tx) => { - const data = await selectPORfqs(tx, { - where: finalWhere, - orderBy, - offset, - limit: input.perPage, - }); - - const total = await countPORfqs(tx, finalWhere); - return { data, total }; - }); - - console.log(total) - - console.log("쿼리 결과 데이터:", data.length); - - const pageCount = Math.ceil(total / input.perPage); - - return { data, pageCount ,total }; - } catch (err) { - console.error("getRfqs 에러:", err); - - // 에러 세부 정보 더 자세히 로깅 - if (err instanceof Error) { - console.error("에러 메시지:", err.message); - console.error("에러 스택:", err.stack); - - if ('code' in err) { - console.error("SQL 에러 코드:", (err as any).code); - } - } - - // 에러 발생 시 디폴트 - return { data: [], pageCount: 0 }; - } - }, - [JSON.stringify(input)], - { - revalidate: 3600, - tags: [`rfqs-po`], - } - )(); -} - -// RFQ 디테일 데이터를 가져오는 함수 -export async function getRfqDetails(rfqId: number) { - return unstable_cache( - async () => { - try { - unstable_noStore(); - - // SQL 쿼리 직접 실행 - const data = await db - .select() - .from(procurementRfqDetailsView) - .where(eq(procurementRfqDetailsView.rfqId, rfqId)) - - console.log(`RFQ 디테일 SQL 조회 완료: ${rfqId}, ${data?.length}건`); - - return { data }; - } catch (err) { - console.error("RFQ 디테일 SQL 조회 오류:", err); - - if (err instanceof Error) { - console.error("에러 메시지:", err.message); - console.error("에러 스택:", err.stack); - } - - return { data: [] }; - } - }, - [`rfq-details-sql-${rfqId}`], - { - revalidate: 60, - tags: [`rfq-details-${rfqId}`], - } - )(); -} - -// RFQ ID로 디테일 데이터를 가져오는 서버 액션 -export async function fetchRfqDetails(rfqId: number) { - "use server"; - - try { - const result = await getRfqDetails(rfqId); - return result; - } catch (error) { - console.error("RFQ 디테일 서버 액션 오류:", error); - return { data: [] }; - } -} - -// RFQ ID로 PR 상세 항목들을 가져오는 함수 -export async function getPrItemsByRfqId(rfqId: number) { - return unstable_cache( - async () => { - try { - unstable_noStore(); - - - const data = await db - .select() - .from(prItemsView) - .where(eq(prItemsView.procurementRfqsId, rfqId)) - - - console.log(`PR 항목 조회 완료: ${rfqId}, ${data.length}건`); - - return { data }; - } catch (err) { - console.error("PR 항목 조회 오류:", err); - - if (err instanceof Error) { - console.error("에러 메시지:", err.message); - console.error("에러 스택:", err.stack); - } - - return { data: [] }; - } - }, - [`pr-items-${rfqId}`], - { - revalidate: 60, // 1분 캐시 - tags: [`pr-items-${rfqId}`], - } - )(); -} - -// 서버 액션으로 노출할 함수 -export async function fetchPrItemsByRfqId(rfqId: number) { - "use server"; - - try { - const result = await getPrItemsByRfqId(rfqId); - return result; - } catch (error) { - console.error("PR 항목 서버 액션 오류:", error); - return { data: [] }; - } -} - -export async function addVendorToRfq(formData: FormData) { - try { - // 현재 사용자 정보 가져오기 - const { userId, user } = await getAuthenticatedUser(); - console.log("userId", userId); - // rfqId 가져오기 - const rfqId = Number(formData.get("rfqId")) - - if (!rfqId) { - return { - success: false, - message: "RFQ ID가 필요합니다", - } - } - - // 폼 데이터 추출 및 기본 검증 (기존과 동일) - const vendorId = Number(formData.get("vendorId")) - const currency = formData.get("currency") as string - const paymentTermsCode = formData.get("paymentTermsCode") as string - const incotermsCode = formData.get("incotermsCode") as string - const incotermsDetail = formData.get("incotermsDetail") as string || null - const deliveryDate = formData.get("deliveryDate") ? new Date(formData.get("deliveryDate") as string) : null - const taxCode = formData.get("taxCode") as string || null - const placeOfShipping = formData.get("placeOfShipping") as string || null - const placeOfDestination = formData.get("placeOfDestination") as string || null - const materialPriceRelatedYn = formData.get("materialPriceRelatedYn") === "true" - - if (!vendorId || !currency || !paymentTermsCode || !incotermsCode) { - return { - success: false, - message: "필수 항목이 누락되었습니다", - } - } - - // 트랜잭션 시작 - return await db.transaction(async (tx) => { - // 0. 먼저 RFQ 상태 확인 - const rfq = await tx.query.procurementRfqs.findFirst({ - where: eq(procurementRfqs.id, rfqId), - columns: { - id: true, - status: true - } - }); - - if (!rfq) { - throw new Error("RFQ를 찾을 수 없습니다"); - } - console.log("rfq.status", rfq.status); - // 1. RFQ 상세 정보 저장 - const insertedDetails = await tx.insert(procurementRfqDetails).values({ - procurementRfqsId: rfqId, - vendorsId: vendorId, - currency, - paymentTermsCode, - incotermsCode, - incotermsDetail, - deliveryDate: deliveryDate || new Date(), // null이면 현재 날짜 사용 - taxCode, - placeOfShipping, - placeOfDestination, - materialPriceRelatedYn, - updatedBy: Number(userId), - updatedAt: new Date(), - }).returning({ id: procurementRfqDetails.id }); - - if (!insertedDetails || insertedDetails.length === 0) { - throw new Error("RFQ 상세 정보 저장에 실패했습니다"); - } - - const detailId = insertedDetails[0].id; - - - - // 2. RFQ 상태가 "RFQ Created"인 경우 "RFQ Vendor Assignned"로 업데이트 - let statusUpdated = false; - if (rfq.status === "RFQ Created") { - console.log("rfq 상태 업데이트 시작") - await tx.update(procurementRfqs) - .set({ - status: "RFQ Vendor Assignned", - updatedBy: Number(userId), - updatedAt: new Date() - }) - .where(eq(procurementRfqs.id, rfqId)); - - statusUpdated = true; - } - - // 3. 첨부 파일 처리 - const filePromises = []; - const uploadDir = join(process.cwd(), "public", "rfq", rfqId.toString(), "vendors", detailId.toString()); - - // 업로드 디렉토리 생성 - try { - await mkdir(uploadDir, { recursive: true }); - } catch (error) { - console.error("디렉토리 생성 오류:", error); - } - - // FormData에서 file 타입 항목 찾기 - for (const [key, value] of formData.entries()) { - if (key.startsWith("attachment-") && value instanceof File) { - const file = value as File; - - // 파일 크기가 0이면 건너뛰기 - if (file.size === 0) continue; - - // 파일 이름 생성 - const uniqueId = uuidv4(); - const fileName = `${uniqueId}-${file.name.replace(/[^a-zA-Z0-9._-]/g, '_')}`; - const filePath = join(uploadDir, fileName); - - // 파일을 버퍼로 변환 - const buffer = Buffer.from(await file.arrayBuffer()); - - // 파일 저장 - await writeFile(filePath, buffer); - - // DB에 첨부 파일 정보 저장 - filePromises.push( - tx.insert(procurementAttachments).values({ - attachmentType: 'VENDOR_SPECIFIC', - procurementRfqsId: null, - procurementRfqDetailsId: detailId, - fileName: fileName, - originalFileName: file.name, - filePath: `/uploads/rfq/${rfqId}/vendors/${detailId}/${fileName}`, - fileSize: file.size, - fileType: file.type, - description: `${file.name} - 벤더 ID ${vendorId}용 첨부파일`, - createdBy: Number(userId), - createdAt: new Date(), - }) - ); - } - } - - // 첨부 파일이 있으면 처리 - if (filePromises.length > 0) { - await Promise.all(filePromises); - } - - // 캐시 무효화 (여러 경로 지정 가능) - revalidateTag(`rfq-details-${rfqId}`); - revalidateTag(`rfqs-po`); - - return { - success: true, - message: "벤더 정보가 성공적으로 추가되었습니다", - data: { - id: detailId, - statusUpdated: statusUpdated - }, - }; - }); - - } catch (error) { - console.error("벤더 추가 오류:", error); - return { - success: false, - message: "벤더 추가 중 오류가 발생했습니다: " + (error instanceof Error ? error.message : String(error)), - }; - } -} - - -// 벤더 데이터 조회 서버 액션 -export async function fetchVendors() { - try { - const data = await db.select().from(vendors) - - return { - success: true, - data, - } - } catch (error) { - console.error("벤더 데이터 로드 오류:", error) - return { - success: false, - message: "벤더 데이터를 불러오는 데 실패했습니다", - data: [] - } - } -} - -// 통화 데이터 조회 서버 액션 -export async function fetchCurrencies() { - try { - // 통화 테이블이 별도로 없다면 여기서 하드코딩하거나 설정 파일에서 가져올 수도 있습니다 - const data = [ - { code: "KRW", name: "Korean Won" }, - { code: "USD", name: "US Dollar" }, - { code: "EUR", name: "Euro" }, - { code: "JPY", name: "Japanese Yen" }, - { code: "CNY", name: "Chinese Yuan" }, - ] - - return { - success: true, - data, - } - } catch (error) { - console.error("통화 데이터 로드 오류:", error) - return { - success: false, - message: "통화 데이터를 불러오는 데 실패했습니다", - data: [] - } - } -} - -// 지불 조건 데이터 조회 서버 액션 -export async function fetchPaymentTerms() { - try { - const data = await db.select().from(paymentTerms) - - return { - success: true, - data, - } - } catch (error) { - console.error("지불 조건 데이터 로드 오류:", error) - return { - success: false, - message: "지불 조건 데이터를 불러오는 데 실패했습니다", - data: [] - } - } -} - -// 인코텀즈 데이터 조회 서버 액션 -export async function fetchIncoterms() { - try { - const data = await db.select().from(incoterms) - - return { - success: true, - data, - } - } catch (error) { - console.error("인코텀즈 데이터 로드 오류:", error) - return { - success: false, - message: "인코텀즈 데이터를 불러오는 데 실패했습니다", - data: [] - } - } -} - -export async function deleteRfqDetail(detailId: number) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session || !session.user) { - return { - success: false, - message: "인증이 필요합니다", - }; - } - - // DB에서 항목 삭제 - await db.delete(procurementRfqDetails) - .where(eq(procurementRfqDetails.id, detailId)); - - // 캐시 무효화 - revalidateTag(`rfq-details-${detailId}`); - - return { - success: true, - message: "RFQ 벤더 정보가 삭제되었습니다", - }; - } catch (error) { - console.error("RFQ 벤더 정보 삭제 오류:", error); - return { - success: false, - message: "RFQ 벤더 정보 삭제 중 오류가 발생했습니다", - }; - } -} - -// RFQ 상세 정보 수정 -export async function updateRfqDetail(detailId: number, data: any) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session || !session.user) { - return { - success: false, - message: "인증이 필요합니다", - }; - } - - const userId = Number(session.user.id); - - // 필요한 데이터 추출 - const { - vendorId, - currency, - paymentTermsCode, - incotermsCode, - incotermsDetail, - deliveryDate, - taxCode, - placeOfShipping, - placeOfDestination, - materialPriceRelatedYn, - } = data; - - // DB 업데이트 - await db.update(procurementRfqDetails) - .set({ - vendorsId: Number(vendorId), - currency, - paymentTermsCode, - incotermsCode, - incotermsDetail: incotermsDetail || null, - deliveryDate: deliveryDate ? new Date(deliveryDate) : new Date(), - taxCode: taxCode || null, - placeOfShipping: placeOfShipping || null, - placeOfDestination: placeOfDestination || null, - materialPriceRelatedYn, - updatedBy: userId, - updatedAt: new Date(), - }) - .where(eq(procurementRfqDetails.id, detailId)); - - // 캐시 무효화 - revalidateTag(`rfq-details-${detailId}`); - - return { - success: true, - message: "RFQ 벤더 정보가 수정되었습니다", - }; - } catch (error) { - console.error("RFQ 벤더 정보 수정 오류:", error); - return { - success: false, - message: "RFQ 벤더 정보 수정 중 오류가 발생했습니다", - }; - } -} - -export async function updateRfqRemark(rfqId: number, remark: string) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session || !session.user) { - return { - success: false, - message: "인증이 필요합니다", - }; - } - - console.log(rfqId, remark) - - // DB 업데이트 - await db.update(procurementRfqs) - .set({ - remark, - updatedBy: Number(session.user.id), - updatedAt: new Date(), - }) - .where(eq(procurementRfqs.id, rfqId)); - - // 캐시 무효화 - revalidateTag(`rfqs-po`); - revalidatePath("/evcp/po-rfq"); // 경로도 함께 무효화 - - return { - success: true, - message: "비고가 업데이트되었습니다", - }; - } catch (error) { - console.error("비고 업데이트 오류:", error); - return { - success: false, - message: "비고 업데이트 중 오류가 발생했습니다", - }; - } -} - -export async function sealRfq(rfqId: number) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session || !session.user) { - return { - success: false, - message: "인증이 필요합니다", - }; - } - - // DB 업데이트 - await db.update(procurementRfqs) - .set({ - rfqSealedYn: true, - updatedBy: Number(session.user.id), - updatedAt: new Date(), - }) - .where(eq(procurementRfqs.id, rfqId)); - - // 캐시 무효화 - revalidateTag(`rfqs-po`); - - return { - success: true, - message: "RFQ가 성공적으로 밀봉되었습니다", - }; - } catch (error) { - console.error("RFQ 밀봉 오류:", error); - return { - success: false, - message: "RFQ 밀봉 중 오류가 발생했습니다", - }; - } -} - -// RFQ 전송 서버 액션 -export async function sendRfq(rfqId: number) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session?.user) { - return { - success: false, - message: "인증이 필요합니다", - } - } - - // 현재 RFQ 상태 확인 - // RFQ 및 관련 정보 조회 - const rfq = await db.query.procurementRfqs.findFirst({ - where: eq(procurementRfqs.id, rfqId), - columns: { - id: true, - rfqCode: true, - status: true, - dueDate: true, - rfqSendDate: true, - remark: true, - rfqSealedYn: true, - itemCode: true, - itemName: true, - }, - with: { - project: { - columns: { - id: true, - code: true, - name: true, - } - }, - createdByUser: { - columns: { - id: true, - name: true, - email: true, - } - }, - prItems: { - columns: { - id: true, - rfqItem: true, // 아이템 번호 - materialCode: true, - materialDescription: true, - quantity: true, - uom: true, - prNo: true, - majorYn: true, - } - } - } - }); - - if (!rfq) { - return { - success: false, - message: "RFQ를 찾을 수 없습니다", - } - } - - if (rfq.status !== "RFQ Vendor Assignned" && rfq.status !== "RFQ Sent") { - return { - success: false, - message: "벤더가 할당된 RFQ 또는 이미 전송된 RFQ만 다시 전송할 수 있습니다", - } - } - - const isResend = rfq.status === "RFQ Sent"; - - // 현재 사용자 정보 조회 (CC 용) - const sender = await db.query.users.findFirst({ - where: eq(users.id, Number(session.user.id)), - columns: { - id: true, - email: true, - name: true, - } - }); - - if (!sender || !sender.email) { - return { - success: false, - message: "보내는 사람의 이메일 정보를 찾을 수 없습니다", - } - } - - // RFQ에 할당된 벤더 목록 조회 - const rfqDetails = await db.query.procurementRfqDetails.findMany({ - where: eq(procurementRfqDetails.procurementRfqsId, rfqId), - columns: { - id: true, - vendorsId: true, - currency: true, - paymentTermsCode: true, - incotermsCode: true, - incotermsDetail: true, - deliveryDate: true, - }, - with: { - vendor: { - columns: { - id: true, - vendorName: true, - vendorCode: true, - } - }, - paymentTerms: { - columns: { - code: true, - description: true, - } - }, - incoterms: { - columns: { - code: true, - description: true, - } - } - } - }); - - if (rfqDetails.length === 0) { - return { - success: false, - message: "할당된 벤더가 없습니다", - } - } - - // 트랜잭션 시작 - await db.transaction(async (tx) => { - // 1. RFQ 상태 업데이트 - await tx.update(procurementRfqs) - .set({ - status: "RFQ Sent", - rfqSendDate: new Date(), - updatedBy: Number(session.user.id), - updatedAt: new Date(), - }) - .where(eq(procurementRfqs.id, rfqId)); - - // 2. 각 벤더에 대해 초기 견적서 레코드 생성 및 이메일 발송 - for (const detail of rfqDetails) { - if (!detail.vendorsId || !detail.vendor) continue; - - // 기존 Draft 견적서가 있는지 확인 - const existingQuotation = await tx.query.procurementVendorQuotations.findFirst({ - where: and( - eq(procurementVendorQuotations.rfqId, rfqId), - eq(procurementVendorQuotations.vendorId, detail.vendorsId) - ), - orderBy: [desc(procurementVendorQuotations.quotationVersion)] - }); - - // 견적서 코드 (기존 것 재사용 또는 신규 생성) - const quotationCode = existingQuotation?.quotationCode || `${rfq.rfqCode}-${detail.vendorsId}`; - - // 버전 관리 - 재전송인 경우 버전 증가 - const quotationVersion = existingQuotation ? ((existingQuotation.quotationVersion? existingQuotation.quotationVersion: 0 )+ 1) : 1; - - // 견적서 레코드 생성 - const insertedQuotation = await tx.insert(procurementVendorQuotations).values({ - rfqId, - vendorId: detail.vendorsId, - quotationCode, - quotationVersion, - totalItemsCount: rfq.prItems.length, - subTotal: "0", - taxTotal: "0", - discountTotal: "0", - totalPrice: "0", - currency: detail.currency || "USD", - // 납품일은 RFQ 납품일보다 조금 이전으로 설정 (기본값) - estimatedDeliveryDate: detail.deliveryDate ? - new Date(detail.deliveryDate.getTime() - 7 * 24 * 60 * 60 * 1000) : // 1주일 전 - undefined, - paymentTermsCode: detail.paymentTermsCode, - incotermsCode: detail.incotermsCode, - incotermsDetail: detail.incotermsDetail, - status: "Draft", - createdBy: Number(session.user.id), - updatedBy: Number(session.user.id), - createdAt: new Date(), - updatedAt: new Date(), - }).returning({ id: procurementVendorQuotations.id }); - - // 새로 생성된 견적서 ID - const quotationId = insertedQuotation[0].id; - - // 3. 각 PR 아이템에 대해 견적 아이템 생성 - for (const prItem of rfq.prItems) { - // procurementQuotationItems에 레코드 생성 - await tx.insert(procurementQuotationItems).values({ - quotationId, - prItemId: prItem.id, - materialCode: prItem.materialCode, - materialDescription: prItem.materialDescription, - quantity: prItem.quantity, - uom: prItem.uom, - // 기본값으로 설정된 필드 - unitPrice: 0, - totalPrice: 0, - currency: detail.currency || "USD", - // 나머지 필드는 null 또는 기본값 사용 - createdAt: new Date(), - updatedAt: new Date(), - }); - } - - // 벤더에 속한 모든 사용자 조회 - const vendorUsers = await db.query.users.findMany({ - where: eq(users.companyId, detail.vendorsId), - columns: { - id: true, - email: true, - name: true, - language: true - } - }); - - // 유효한 이메일 주소만 필터링 - const vendorEmailsString = vendorUsers - .filter(user => user.email) - .map(user => user.email) - .join(", "); - - if (vendorEmailsString) { - // 대표 언어 결정 (첫 번째 사용자의 언어 또는 기본값) - const language = vendorUsers[0]?.language || "en"; - - // 이메일 컨텍스트 구성 - const emailContext = { - language: language, - rfq: { - id: rfq.id, - code: rfq.rfqCode, - title: rfq.item?.itemName || '', - projectCode: rfq.project?.code || '', - projectName: rfq.project?.name || '', - description: rfq.remark || '', - dueDate: rfq.dueDate ? formatDate(rfq.dueDate, "KR") : 'N/A', - deliveryDate: detail.deliveryDate ? formatDate(detail.deliveryDate, "KR") : 'N/A', - }, - vendor: { - id: detail.vendor.id, - code: detail.vendor.vendorCode || '', - name: detail.vendor.vendorName, - }, - sender: { - fullName: sender.name || '', - email: sender.email, - }, - items: rfq.prItems.map(item => ({ - itemNumber: item.rfqItem || '', - materialCode: item.materialCode || '', - description: item.materialDescription || '', - quantity: item.quantity, - uom: item.uom || '', - })), - details: { - currency: detail.currency || 'USD', - paymentTerms: detail.paymentTerms?.description || detail.paymentTermsCode || 'N/A', - incoterms: detail.incoterms ? - `${detail.incoterms.code} ${detail.incotermsDetail || ''}` : - detail.incotermsCode ? `${detail.incotermsCode} ${detail.incotermsDetail || ''}` : 'N/A', - }, - quotationCode: existingQuotation?.quotationCode || `QUO-${rfqId}-${detail.vendorsId}`, - systemUrl: process.env.NEXT_PUBLIC_APP_URL || 'https://evcp.com', - isResend: isResend, - quotationVersion: quotationVersion, - versionInfo: isResend ? `(버전 ${quotationVersion})` : '', - }; - - // 이메일 전송 (모든 벤더 이메일을 to 필드에 배열로 전달) - await sendEmail({ - to: vendorEmailsString, - subject: isResend - ? `[RFQ 재전송] ${rfq.rfqCode} - ${rfq.item?.itemName || '견적 요청'} ${emailContext.versionInfo}` - : `[RFQ] ${rfq.rfqCode} - ${rfq.item?.itemName || '견적 요청'}`, - template: 'rfq-notification', - context: emailContext, - cc: sender.email, // 발신자를 CC에 추가 - }); - } - } - }); - - // 캐시 무효화 - revalidateTag(`rfqs-po`); - - return { - success: true, - message: "RFQ가 성공적으로 전송되었습니다", - } - } catch (error) { - console.error("RFQ 전송 오류:", error); - return { - success: false, - message: "RFQ 전송 중 오류가 발생했습니다", - } - } -} -/** - * 첨부파일 타입 정의 - */ -export interface Attachment { - id: number - fileName: string - fileSize: number - fileType: string | null // <- null 허용 - filePath: string - uploadedAt: Date -} - -/** - * 코멘트 타입 정의 - */ -export interface Comment { - id: number - rfqId: number - vendorId: number | null // null 허용으로 변경 - userId?: number | null // null 허용으로 변경 - content: string - isVendorComment: boolean | null // null 허용으로 변경 - createdAt: Date - updatedAt: Date - userName?: string | null // null 허용으로 변경 - vendorName?: string | null // null 허용으로 변경 - attachments: Attachment[] - isRead: boolean | null // null 허용으로 변경 -} - - -/** - * 특정 RFQ와 벤더 간의 커뮤니케이션 메시지를 가져오는 서버 액션 - * - * @param rfqId RFQ ID - * @param vendorId 벤더 ID - * @returns 코멘트 목록 - */ -export async function fetchVendorComments(rfqId: number, vendorId?: number): Promise { - if (!vendorId) { - return [] - } - - try { - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session?.user) { - throw new Error("인증이 필요합니다") - } - - // 코멘트 쿼리 - const comments = await db.query.procurementRfqComments.findMany({ - where: and( - eq(procurementRfqComments.rfqId, rfqId), - eq(procurementRfqComments.vendorId, vendorId) - ), - orderBy: [procurementRfqComments.createdAt], - with: { - user: { - columns: { - name: true - } - }, - vendor: { - columns: { - vendorName: true - } - }, - attachments: true, - } - }) - - // 결과 매핑 - return comments.map(comment => ({ - id: comment.id, - rfqId: comment.rfqId, - vendorId: comment.vendorId, - userId: comment.userId || undefined, - content: comment.content, - isVendorComment: comment.isVendorComment, - createdAt: comment.createdAt, - updatedAt: comment.updatedAt, - userName: comment.user?.name, - vendorName: comment.vendor?.vendorName, - isRead: comment.isRead, - attachments: comment.attachments.map(att => ({ - id: att.id, - fileName: att.fileName, - fileSize: att.fileSize, - fileType: att.fileType, - filePath: att.filePath, - uploadedAt: att.uploadedAt - })) - })) - } catch (error) { - console.error('벤더 코멘트 가져오기 오류:', error) - throw error - } -} - -/** - * 코멘트를 읽음 상태로 표시하는 서버 액션 - * - * @param rfqId RFQ ID - * @param vendorId 벤더 ID - */ -export async function markMessagesAsRead(rfqId: number, vendorId?: number): Promise { - if (!vendorId) { - return - } - - try { - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session?.user) { - throw new Error("인증이 필요합니다") - } - - // 벤더가 작성한 읽지 않은 코멘트 업데이트 - await db.update(procurementRfqComments) - .set({ isRead: true }) - .where( - and( - eq(procurementRfqComments.rfqId, rfqId), - eq(procurementRfqComments.vendorId, vendorId), - eq(procurementRfqComments.isVendorComment, true), - eq(procurementRfqComments.isRead, false) - ) - ) - - // 캐시 무효화 - revalidateTag(`rfq-${rfqId}-comments`) - } catch (error) { - console.error('메시지 읽음 표시 오류:', error) - throw error - } -} - -/** - * 읽지 않은 메시지 개수 가져오기 서버 액션 - * - * @param rfqId RFQ ID - */ -export async function fetchUnreadMessages(rfqId: number): Promise> { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - if (!session?.user) { - throw new Error("인증이 필요합니다"); - } - - // 쿼리 빌더 방식으로 카운트 조회 - 타입 안전 방식 - const result = await db - .select({ - vendorId: procurementRfqComments.vendorId, - unreadCount: count() - }) - .from(procurementRfqComments) - .where( - and( - eq(procurementRfqComments.rfqId, rfqId), - eq(procurementRfqComments.isVendorComment, true), - eq(procurementRfqComments.isRead, false) - ) - ) - .groupBy(procurementRfqComments.vendorId); - - // 결과 매핑 - const unreadMessages: Record = {}; - result.forEach(row => { - if (row.vendorId) { - unreadMessages[row.vendorId] = Number(row.unreadCount); - } - }); - - return unreadMessages; - } catch (error) { - console.error('읽지 않은 메시지 개수 가져오기 오류:', error); - throw error; - } -} - - -/** - * 견적서 업데이트 서버 액션 - */ -export async function updateVendorQuotation(data: { - id: number - quotationVersion?: number - currency?: string - validUntil?: Date - estimatedDeliveryDate?: Date - paymentTermsCode?: string - incotermsCode?: string - incotermsDetail?: string - remark?: string - subTotal?: string - taxTotal?: string - discountTotal?: string - totalPrice?: string - totalItemsCount?: number -}) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session?.user) { - return { - success: false, - message: "인증이 필요합니다", - } - } - - // 견적서 존재 확인 - const quotation = await db.query.procurementVendorQuotations.findFirst({ - where: eq(procurementVendorQuotations.id, data.id), - }) - - if (!quotation) { - return { - success: false, - message: "견적서를 찾을 수 없습니다", - } - } - - // 권한 확인 (벤더 또는 관리자만 수정 가능) - const isAuthorized = - (session.user.domain === "partners" && session.user.companyId === quotation.vendorId) - - if (!isAuthorized) { - return { - success: false, - message: "견적서 수정 권한이 없습니다", - } - } - - // 상태 확인 (Draft 또는 Rejected 상태만 수정 가능) - if (quotation.status !== "Draft" && quotation.status !== "Rejected") { - return { - success: false, - message: "제출되었거나 승인된 견적서는 수정할 수 없습니다", - } - } - - // 업데이트할 데이터 구성 - const updateData: Record = { - updatedBy: Number(session.user.id), - updatedAt: new Date(), - } - - // 필드 추가 - if (data.currency) updateData.currency = data.currency - if (data.validUntil) updateData.validUntil = data.validUntil - if (data.estimatedDeliveryDate) updateData.estimatedDeliveryDate = data.estimatedDeliveryDate - if (data.paymentTermsCode) updateData.paymentTermsCode = data.paymentTermsCode - if (data.incotermsCode) updateData.incotermsCode = data.incotermsCode - if (data.incotermsDetail !== undefined) updateData.incotermsDetail = data.incotermsDetail - if (data.remark !== undefined) updateData.remark = data.remark - if (data.subTotal) updateData.subTotal = data.subTotal - if (data.taxTotal) updateData.taxTotal = data.taxTotal - if (data.discountTotal) updateData.discountTotal = data.discountTotal - if (data.totalPrice) updateData.totalPrice = data.totalPrice - if (data.totalItemsCount) updateData.totalItemsCount = data.totalItemsCount - - // Rejected 상태에서 수정 시 Draft 상태로 변경 - if (quotation.status === "Rejected") { - updateData.status = "Draft" - - // 버전 증가 - if (data.quotationVersion) { - updateData.quotationVersion = data.quotationVersion + 1 - } else { - updateData.quotationVersion = (quotation.quotationVersion ?? 0) + 1 - } - } - - // 견적서 업데이트 - await db.update(procurementVendorQuotations) - .set(updateData) - .where(eq(procurementVendorQuotations.id, data.id)) - - // 캐시 무효화 - revalidateTag(`quotation-${data.id}`) - revalidateTag(`rfq-${quotation.rfqId}`) - - return { - success: true, - message: "견적서가 업데이트되었습니다", - } - } catch (error) { - console.error("견적서 업데이트 오류:", error) - return { - success: false, - message: "견적서 업데이트 중 오류가 발생했습니다", - } - } -} - -interface QuotationItem { - unitPrice: number; - deliveryDate: Date | null; - status: "Draft" | "Rejected" | "Submitted" | "Approved"; // 상태를 유니온 타입으로 정의 - - // 필요한 다른 속성들도 추가 -} - -/** - * 견적서 제출 서버 액션 - */ -export async function submitVendorQuotation(data: { - id: number - quotationVersion?: number - currency?: string - validUntil?: Date - estimatedDeliveryDate?: Date - paymentTermsCode?: string - incotermsCode?: string - incotermsDetail?: string - remark?: string - subTotal?: string - taxTotal?: string - discountTotal?: string - totalPrice?: string - totalItemsCount?: number -}) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session?.user) { - return { - success: false, - message: "인증이 필요합니다", - } - } - - // 견적서 존재 확인 - const quotation = await db.query.procurementVendorQuotations.findFirst({ - where: eq(procurementVendorQuotations.id, data.id), - with: { - items: true, - } - }) - - if (!quotation) { - return { - success: false, - message: "견적서를 찾을 수 없습니다", - } - } - - // 권한 확인 (벤더 또는 관리자만 제출 가능) - const isAuthorized = - (session.user.domain === "partners" && session.user.companyId === quotation.vendorId) - - if (!isAuthorized) { - return { - success: false, - message: "견적서 제출 권한이 없습니다", - } - } - - // 상태 확인 (Draft 또는 Rejected 상태만 제출 가능) - if (quotation.status !== "Draft" && quotation.status !== "Rejected") { - return { - success: false, - message: "이미 제출되었거나 승인된 견적서는 다시 제출할 수 없습니다", - } - } - - // 견적 항목 검증 - if (!quotation.items || (quotation.items as QuotationItem[]).length === 0) { - return { - success: false, - message: "견적 항목이 없습니다", - } - } - - // 필수 항목 검증 - const hasEmptyItems = (quotation.items as QuotationItem[]).some(item => - item.unitPrice <= 0 || !item.deliveryDate - ) - - if (hasEmptyItems) { - return { - success: false, - message: "모든 항목의 단가와 납품일을 입력해주세요", - } - } - - // 필수 정보 검증 - if (!data.validUntil || !data.estimatedDeliveryDate) { - return { - success: false, - message: "견적 유효기간과 예상 납품일은 필수 항목입니다", - } - } - - // 업데이트할 데이터 구성 - const updateData: Record = { - status: "Submitted", - submittedAt: new Date(), - updatedBy: Number(session.user.id), - updatedAt: new Date(), - } - - // 필드 추가 - if (data.currency) updateData.currency = data.currency - if (data.validUntil) updateData.validUntil = data.validUntil - if (data.estimatedDeliveryDate) updateData.estimatedDeliveryDate = data.estimatedDeliveryDate - if (data.paymentTermsCode) updateData.paymentTermsCode = data.paymentTermsCode - if (data.incotermsCode) updateData.incotermsCode = data.incotermsCode - if (data.incotermsDetail !== undefined) updateData.incotermsDetail = data.incotermsDetail - if (data.remark !== undefined) updateData.remark = data.remark - if (data.subTotal) updateData.subTotal = data.subTotal - if (data.taxTotal) updateData.taxTotal = data.taxTotal - if (data.discountTotal) updateData.discountTotal = data.discountTotal - if (data.totalPrice) updateData.totalPrice = data.totalPrice - if (data.totalItemsCount) updateData.totalItemsCount = data.totalItemsCount - - // Rejected 상태에서 제출 시 버전 증가 - if (quotation.status === "Rejected") { - updateData.status = "Revised" - - if (data.quotationVersion) { - updateData.quotationVersion = data.quotationVersion + 1 - } else { - updateData.quotationVersion = (quotation.quotationVersion ?? 0) + 1 - } - } - - // 견적서 업데이트 - await db.update(procurementVendorQuotations) - .set(updateData) - .where(eq(procurementVendorQuotations.id, data.id)) - - // 캐시 무효화 - revalidateTag(`quotation-${data.id}`) - revalidateTag(`rfq-${quotation.rfqId}`) - - return { - success: true, - message: "견적서가 성공적으로 제출되었습니다", - } - } catch (error) { - console.error("견적서 제출 오류:", error) - return { - success: false, - message: "견적서 제출 중 오류가 발생했습니다", - } - } -} - -/** - * 견적 항목 업데이트 서버 액션 - */ -export async function updateQuotationItem(data: { - id: number - unitPrice?: number - totalPrice?: number - vendorMaterialCode?: string - vendorMaterialDescription?: string - deliveryDate?: Date | null - leadTimeInDays?: number - taxRate?: number - taxAmount?: number - discountRate?: number - discountAmount?: number - remark?: string - isAlternative?: boolean - isRecommended?: boolean -}) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session?.user) { - return { - success: false, - message: "인증이 필요합니다", - } - } - - // 항목 존재 확인 - const item = await db.query.procurementQuotationItems.findFirst({ - where: eq(procurementQuotationItems.id, data.id), - with: { - quotation: true, - } - }) - - if (!item || !item.quotation) { - return { - success: false, - message: "견적 항목을 찾을 수 없습니다", - } - } - - // 권한 확인 (벤더 또는 관리자만 수정 가능) - const isAuthorized = ( - session.user.domain === "partners" && - session.user.companyId === (item.quotation as { vendorId: number }).vendorId - ) - - if (!isAuthorized) { - return { - success: false, - message: "견적 항목 수정 권한이 없습니다", - } - } - - const quotation = item.quotation as Quotation; - - // 상태 확인 (Draft 또는 Rejected 상태만 수정 가능) - if (quotation.status !== "Draft" && quotation.status !== "Rejected") { - return { - success: false, - message: "제출되었거나 승인된 견적서의 항목은 수정할 수 없습니다", - } - } - - // 업데이트할 데이터 구성 - const updateData: Record = { - updatedAt: new Date(), - } - - // 필드 추가 - if (data.unitPrice !== undefined) updateData.unitPrice = data.unitPrice - if (data.totalPrice !== undefined) updateData.totalPrice = data.totalPrice - if (data.vendorMaterialCode !== undefined) updateData.vendorMaterialCode = data.vendorMaterialCode - if (data.vendorMaterialDescription !== undefined) updateData.vendorMaterialDescription = data.vendorMaterialDescription - if (data.deliveryDate !== undefined) updateData.deliveryDate = data.deliveryDate - if (data.leadTimeInDays !== undefined) updateData.leadTimeInDays = data.leadTimeInDays - if (data.taxRate !== undefined) updateData.taxRate = data.taxRate - if (data.taxAmount !== undefined) updateData.taxAmount = data.taxAmount - if (data.discountRate !== undefined) updateData.discountRate = data.discountRate - if (data.discountAmount !== undefined) updateData.discountAmount = data.discountAmount - if (data.remark !== undefined) updateData.remark = data.remark - if (data.isAlternative !== undefined) updateData.isAlternative = data.isAlternative - if (data.isRecommended !== undefined) updateData.isRecommended = data.isRecommended - - // 항목 업데이트 - await db.update(procurementQuotationItems) - .set(updateData) - .where(eq(procurementQuotationItems.id, data.id)) - - // 캐시 무효화 - revalidateTag(`quotation-${item.quotationId}`) - - return { - success: true, - message: "견적 항목이 업데이트되었습니다", - } - } catch (error) { - console.error("견적 항목 업데이트 오류:", error) - return { - success: false, - message: "견적 항목 업데이트 중 오류가 발생했습니다", - } - } -} - - -// Quotation 상태 타입 정의 -export type QuotationStatus = "Draft" | "Submitted" | "Revised" | "Rejected" | "Accepted"; - -// 인터페이스 정의 -export interface Quotation { - id: number; - quotationCode: string; - status: QuotationStatus; - totalPrice: string; - currency: string; - submittedAt: string | null; - validUntil: string | null; - vendorId: number; - rfq?: { - rfqCode: string; - } | null; - vendor?: any; -} - - -/** - * 벤더별 견적서 목록 조회 - */ -export async function getVendorQuotations(input: GetQuotationsSchema, vendorId: string) { - return unstable_cache( - async () => { - try { - // 페이지네이션 설정 - const page = input.page || 1; - const perPage = input.perPage || 10; - const offset = (page - 1) * perPage; - - // 필터링 설정 - // advancedTable 모드로 where 절 구성 - const advancedWhere = filterColumns({ - table: procurementVendorQuotations, - filters: input.filters, - joinOperator: input.joinOperator, - }); - - // 글로벌 검색 조건 - let globalWhere; - if (input.search) { - const s = `%${input.search}%`; - globalWhere = or( - ilike(procurementVendorQuotations.quotationCode, s), - ilike(procurementVendorQuotations.status, s), - ilike(procurementVendorQuotations.totalPrice, s) - ); - } - - // 벤더 ID 조건 - const vendorIdWhere = vendorId ? - eq(procurementVendorQuotations.vendorId, Number(vendorId)) : - undefined; - - // 모든 조건 결합 - let whereConditions = []; - if (advancedWhere) whereConditions.push(advancedWhere); - if (globalWhere) whereConditions.push(globalWhere); - if (vendorIdWhere) whereConditions.push(vendorIdWhere); - - // 최종 조건 - const finalWhere = whereConditions.length > 0 - ? and(...whereConditions) - : undefined; - - // 정렬 설정 - const orderBy = input.sort && input.sort.length > 0 - ? input.sort.map((item) => { - // @ts-ignore - 동적 속성 접근 - return item.desc ? desc(procurementVendorQuotations[item.id]) : asc(procurementVendorQuotations[item.id]); - }) - : [asc(procurementVendorQuotations.updatedAt)]; - - // 쿼리 실행 - const quotations = await db.query.procurementVendorQuotations.findMany({ - where: finalWhere, - orderBy, - offset, - limit: perPage, - with: { - rfq:true, - vendor: true, - } - }); - // 전체 개수 조회 - const { totalCount } = await db - .select({ totalCount: count() }) - .from(procurementVendorQuotations) - .where(finalWhere || undefined) - .then(rows => rows[0]); - - - // 페이지 수 계산 - const pageCount = Math.ceil(Number(totalCount) / perPage); - - return { - data: quotations as Quotation[], - pageCount - }; - } catch (err) { - console.error("getVendorQuotations 에러:", err); - return { data: [], pageCount: 0 }; - } - }, - [`vendor-quotations-${vendorId}-${JSON.stringify(input)}`], - { - revalidate: 3600, - tags: [`vendor-quotations-${vendorId}`], - } - )(); -} - -/** - * 견적서 상태별 개수 조회 - */ -export async function getQuotationStatusCounts(vendorId: string) { - return unstable_cache( - async () => { - try { - const initial: Record = { - Draft: 0, - Submitted: 0, - Revised: 0, - Rejected: 0, - Accepted: 0, - }; - - // 벤더 ID 조건 - const whereCondition = vendorId ? - eq(procurementVendorQuotations.vendorId, Number(vendorId)) : - undefined; - - // 상태별 그룹핑 쿼리 - const rows = await db - .select({ - status: procurementVendorQuotations.status, - count: count(), - }) - .from(procurementVendorQuotations) - .where(whereCondition) - .groupBy(procurementVendorQuotations.status); - - // 결과 처리 - const result = rows.reduce>((acc, { status, count }) => { - if (status) { - acc[status as QuotationStatus] = Number(count); - } - return acc; - }, initial); - - return result; - } catch (err) { - console.error("getQuotationStatusCounts 에러:", err); - return {} as Record; - } - }, - [`quotation-status-counts-${vendorId}`], - { - revalidate: 3600, - } - )(); -} - - -/** - * 벤더 입장에서 구매자와의 커뮤니케이션 메시지를 가져오는 서버 액션 - * - * @param rfqId RFQ ID - * @param vendorId 벤더 ID - * @returns 코멘트 목록 - */ -export async function fetchBuyerVendorComments(rfqId: number, vendorId: number): Promise { - if (!rfqId || !vendorId) { - return []; - } - - try { - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session?.user) { - throw new Error("인증이 필요합니다"); - } - - // 벤더 접근 권한 확인 (벤더 사용자이며 해당 벤더의 ID와 일치해야 함) - if ( - session.user.domain === "partners" && - ((session.user.companyId ?? 0) !== Number(vendorId)) - ) { - throw new Error("접근 권한이 없습니다"); - } - - // 코멘트 쿼리 - const comments = await db.query.procurementRfqComments.findMany({ - where: and( - eq(procurementRfqComments.rfqId, rfqId), - eq(procurementRfqComments.vendorId, vendorId) - ), - orderBy: [procurementRfqComments.createdAt], - with: { - user: { - columns: { - name: true - } - }, - vendor: { - columns: { - vendorName: true - } - }, - attachments: true, - } - }); - - // 벤더가 접근하는 경우, 벤더 메시지를 읽음 상태로 표시 - if (session.user.domain === "partners") { - // 읽지 않은 구매자 메시지를 읽음 상태로 업데이트 - await db.update(procurementRfqComments) - .set({ isRead: true }) - .where( - and( - eq(procurementRfqComments.rfqId, rfqId), - eq(procurementRfqComments.vendorId, vendorId), - eq(procurementRfqComments.isVendorComment, false), // 구매자가 보낸 메시지 - eq(procurementRfqComments.isRead, false) - ) - ) - .execute(); - } - - // 결과 매핑 - return comments.map(comment => ({ - id: comment.id, - rfqId: comment.rfqId, - vendorId: comment.vendorId, - userId: comment.userId || undefined, - content: comment.content, - isVendorComment: comment.isVendorComment, - createdAt: comment.createdAt, - updatedAt: comment.updatedAt, - userName: comment.user?.name, - vendorName: comment.vendor?.vendorName, - isRead: comment.isRead, - attachments: comment.attachments.map(att => ({ - id: att.id, - fileName: att.fileName, - fileSize: att.fileSize, - fileType: att.fileType, - filePath: att.filePath, - uploadedAt: att.uploadedAt - })) - })); - } catch (error) { - console.error('벤더-구매자 커뮤니케이션 가져오기 오류:', error); - throw error; - } -} - - -const getRandomProject = async () => { - const allProjects = await db.select().from(projects).limit(10); - const randomIndex = Math.floor(Math.random() * allProjects.length); - return allProjects[randomIndex] || null; -}; - -const getRandomItem = async () => { - const allItems = await db.select().from(items).limit(10); - const randomIndex = Math.floor(Math.random() * allItems.length); - return allItems[randomIndex] || null; -}; - -// 외부 시스템에서 RFQ 가져오기 서버 액션 -export async function fetchExternalRfqs() { - try { - // 현재 로그인한 사용자의 세션 정보 가져오기 - const session = await getServerSession(authOptions); - - if (!session || !session.user || !session.user.id) { - return { - success: false, - message: '인증된 사용자 정보를 찾을 수 없습니다' - }; - } - - const userId = session.user.id; - - const randomProject = await getRandomProject(); - const randomItem = await getRandomItem(); - - if (!randomProject || !randomItem) { - return { - success: false, - message: '임의 데이터를 생성하는 데 필요한 기본 데이터가 없습니다' - }; - } - - // 현재 날짜 기준 임의 날짜 생성 - const today = new Date(); - const dueDate = new Date(today); - dueDate.setDate(today.getDate() + Math.floor(Math.random() * 30) + 15); // 15-45일 후 - - - // RFQ 코드 생성 (현재 연도 + 3자리 숫자) - const currentYear = today.getFullYear(); - const randomNum = Math.floor(Math.random() * 900) + 100; // 100-999 - const rfqCode = `R${currentYear}${randomNum}`; - const seriesOptions = ["SS", "II", ""]; - const randomSeriesIndex = Math.floor(Math.random() * seriesOptions.length); - const seriesValue = seriesOptions[randomSeriesIndex]; - - // RFQ 생성 - 로그인한 사용자 ID 사용 - const newRfq = await db.insert(procurementRfqs).values({ - rfqCode, - projectId: randomProject.id, - series:seriesValue, - itemCode: randomItem.itemCode || `ITEM-${Math.floor(Math.random() * 1000)}`, // itemId 대신 itemCode 사용 - itemName: randomItem.itemName || `임의 아이템 ${Math.floor(Math.random() * 100)}`, // itemName 추가 - dueDate, - rfqSendDate: null, // null로 설정/ - status: "RFQ Created", - rfqSealedYn: false, - picCode: `PIC-${Math.floor(Math.random() * 1000)}`, - remark: "테스트용으로 아무말이나 들어간 것으로 실제로는 SAP에 있는 값이 옵니다. 오해 ㄴㄴ", - createdBy: userId, - updatedBy: userId, - }).returning(); - - if (newRfq.length === 0) { - return { - success: false, - message: 'RFQ 생성에 실패했습니다' - }; - } - - // PR 항목 생성 (1-3개 임의 생성) - const prItemsCount = Math.floor(Math.random() * 3) + 1; - const createdPrItems = []; - - for (let i = 0; i < prItemsCount; i++) { - const deliveryDate = new Date(today); - deliveryDate.setDate(today.getDate() + Math.floor(Math.random() * 60) + 30); // 30-90일 후 - - const randomTwoDigits = String(Math.floor(Math.random() * 100)).padStart(2, '0'); - // 프로젝트와 아이템 코드가 있다고 가정하고, 없을 경우 기본값 사용 - const projectCode = randomProject.code || 'PROJ'; - const itemCode = randomItem.itemCode || 'ITEM'; - const materialCode = `${projectCode}${itemCode}${randomTwoDigits}`; - const isMajor = i === 0 ? true : Math.random() > 0.7; - - const newPrItem = await db.insert(prItems).values({ - procurementRfqsId: newRfq[0].id, - rfqItem: `RFQI-${Math.floor(Math.random() * 1000)}`, - prItem: `PRI-${Math.floor(Math.random() * 1000)}`, - prNo: `PRN-${Math.floor(Math.random() * 1000)}`, - // itemId: randomItem.id, - materialCode, - materialCategory: "Standard", - acc: `ACC-${Math.floor(Math.random() * 100)}`, - materialDescription: `${['알루미늄', '구리', '철', '실리콘'][Math.floor(Math.random() * 4)]} 재질 부품`, - size: `${Math.floor(Math.random() * 100) + 10}x${Math.floor(Math.random() * 100) + 10}`, - deliveryDate, - quantity: Math.floor(Math.random() * 100) + 1, - uom: ['EA', 'KG', 'M', 'L'][Math.floor(Math.random() * 4)], - grossWeight: Math.floor(Math.random() * 1000) / 10, - gwUom: ['KG', 'T'][Math.floor(Math.random() * 2)], - specNo: `SPEC-${Math.floor(Math.random() * 1000)}`, - majorYn:isMajor, // 30% 확률로 true - remark: "외부 시스템에서 가져온 PR 항목", - }).returning(); - - createdPrItems.push(newPrItem[0]); - } - - revalidateTag(`rfqs-po`) - - return { - success: true, - message: '외부 RFQ를 성공적으로 가져왔습니다', - data: { - rfq: newRfq[0], - prItems: createdPrItems - } - }; - - } catch (error) { - console.error('외부 RFQ 가져오기 오류:', error); - return { - success: false, - message: '외부 RFQ를 가져오는 중 오류가 발생했습니다' - }; - } -} - -/** - * RFQ ID에 해당하는 모든 벤더 견적 정보를 조회하는 서버 액션 - * @param rfqId RFQ ID - * @returns 견적 정보 목록 - */ -export async function fetchVendorQuotations(rfqId: number) { - try { - // 벤더 정보와 함께 견적 정보 조회 - const quotations = await db - .select({ - // 견적 기본 정보 - id: procurementVendorQuotations.id, - rfqId: procurementVendorQuotations.rfqId, - vendorId: procurementVendorQuotations.vendorId, - quotationCode: procurementVendorQuotations.quotationCode, - quotationVersion: procurementVendorQuotations.quotationVersion, - totalItemsCount: procurementVendorQuotations.totalItemsCount, - subTotal: procurementVendorQuotations.subTotal, - taxTotal: procurementVendorQuotations.taxTotal, - discountTotal: procurementVendorQuotations.discountTotal, - totalPrice: procurementVendorQuotations.totalPrice, - currency: procurementVendorQuotations.currency, - validUntil: procurementVendorQuotations.validUntil, - estimatedDeliveryDate: procurementVendorQuotations.estimatedDeliveryDate, - paymentTermsCode: procurementVendorQuotations.paymentTermsCode, - incotermsCode: procurementVendorQuotations.incotermsCode, - incotermsDetail: procurementVendorQuotations.incotermsDetail, - status: procurementVendorQuotations.status, - remark: procurementVendorQuotations.remark, - rejectionReason: procurementVendorQuotations.rejectionReason, - submittedAt: procurementVendorQuotations.submittedAt, - acceptedAt: procurementVendorQuotations.acceptedAt, - createdAt: procurementVendorQuotations.createdAt, - updatedAt: procurementVendorQuotations.updatedAt, - - // 벤더 정보 - vendorName: vendors.vendorName, - paymentTermsDescription: paymentTerms.description, - incotermsDescription: incoterms.description, - }) - .from(procurementVendorQuotations) - .leftJoin(vendors, eq(procurementVendorQuotations.vendorId, vendors.id)) - .leftJoin(paymentTerms, eq(procurementVendorQuotations.paymentTermsCode, paymentTerms.code)) - .leftJoin(incoterms, eq(procurementVendorQuotations.incotermsCode, incoterms.code)) - .where( - and( - eq(procurementVendorQuotations.rfqId, rfqId), - // eq(procurementVendorQuotations.status, "Submitted") // <=== Submitted 상태만! - ) - ) - .orderBy(desc(procurementVendorQuotations.updatedAt)) - - - return { success: true, data: quotations } - } catch (error) { - console.error("벤더 견적 조회 오류:", error) - return { success: false, error: "벤더 견적을 조회하는 중 오류가 발생했습니다" } - } -} - -/** - * 견적 ID 목록에 해당하는 모든 견적 아이템 정보를 조회하는 서버 액션 - * @param quotationIds 견적 ID 배열 - * @returns 견적 아이템 정보 목록 - */ -export async function fetchQuotationItems(quotationIds: number[]) { - try { - // 빈 배열이 전달된 경우 빈 결과 반환 - if (!quotationIds.length) { - return { success: true, data: [] } - } - - // 견적 아이템 정보 조회 - const items = await db - .select() - .from(procurementQuotationItems) - .where(inArray(procurementQuotationItems.quotationId, quotationIds)) - .orderBy(procurementQuotationItems.id) - - return { success: true, data: items } - } catch (error) { - console.error("견적 아이템 조회 오류:", error) - return { success: false, error: "견적 아이템을 조회하는 중 오류가 발생했습니다" } - } -} - -- cgit v1.2.3