summaryrefslogtreecommitdiff
path: root/lib/procurement-rfqs/services.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-12-01 19:52:06 +0900
committerjoonhoekim <26rote@gmail.com>2025-12-01 19:52:06 +0900
commit44b74ff4170090673b6eeacd8c528e0abf47b7aa (patch)
tree3f3824b4e2cb24536c1677188b4cae5b8909d3da /lib/procurement-rfqs/services.ts
parent4953e770929b82ef77da074f77071ebd0f428529 (diff)
(김준회) deprecated code 정리
Diffstat (limited to 'lib/procurement-rfqs/services.ts')
-rw-r--r--lib/procurement-rfqs/services.ts2050
1 files changed, 0 insertions, 2050 deletions
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<Comment[]> {
- 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<void> {
- 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<Record<number, number>> {
- 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<number, number> = {};
- 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<string, any> = {
- 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<string, any> = {
- 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<string, any> = {
- 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<QuotationStatus, number> = {
- 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<Record<QuotationStatus, number>>((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<QuotationStatus, number>;
- }
- },
- [`quotation-status-counts-${vendorId}`],
- {
- revalidate: 3600,
- }
- )();
-}
-
-
-/**
- * 벤더 입장에서 구매자와의 커뮤니케이션 메시지를 가져오는 서버 액션
- *
- * @param rfqId RFQ ID
- * @param vendorId 벤더 ID
- * @returns 코멘트 목록
- */
-export async function fetchBuyerVendorComments(rfqId: number, vendorId: number): Promise<Comment[]> {
- 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: "견적 아이템을 조회하는 중 오류가 발생했습니다" }
- }
-}
-