diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-17 10:50:52 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-17 10:50:52 +0000 |
| commit | 2ef02e27dbe639876fa3b90c30307dda183545ec (patch) | |
| tree | e132ae7f3dd774e1ce767291c2849be4a63ae762 /lib/tech-vendors/service.ts | |
| parent | fb276ed3db86fe4fc0c0fcd870fd3d085b034be0 (diff) | |
(최겸) 기술영업 변경사항 적용
Diffstat (limited to 'lib/tech-vendors/service.ts')
| -rw-r--r-- | lib/tech-vendors/service.ts | 312 |
1 files changed, 275 insertions, 37 deletions
diff --git a/lib/tech-vendors/service.ts b/lib/tech-vendors/service.ts index da4a44eb..cb5aa89f 100644 --- a/lib/tech-vendors/service.ts +++ b/lib/tech-vendors/service.ts @@ -4,6 +4,7 @@ import { revalidateTag, unstable_noStore } from "next/cache"; import db from "@/db/db"; import { techVendorAttachments, techVendorContacts, techVendorPossibleItems, techVendors, techVendorItemsView, type TechVendor, techVendorCandidates } from "@/db/schema/techVendors"; import { items, itemShipbuilding, itemOffshoreTop, itemOffshoreHull } from "@/db/schema/items"; +import { users } from "@/db/schema/users"; import { filterColumns } from "@/lib/filter-columns"; import { unstable_cache } from "@/lib/unstable-cache"; @@ -32,12 +33,12 @@ import type { CreateTechVendorContactSchema, GetTechVendorItemsSchema, CreateTechVendorItemSchema, + GetTechVendorRfqHistorySchema, } from "./validations"; import { asc, desc, ilike, inArray, and, or, eq, isNull, not } from "drizzle-orm"; import path from "path"; import { sql } from "drizzle-orm"; -import { users } from "@/db/schema/users"; import { decryptWithServerAction } from "@/components/drm/drmUtils"; import { deleteFile, saveDRMFile } from "../file-stroage"; @@ -510,13 +511,16 @@ export async function createTechVendorContact(input: CreateTechVendorContactSche contactPosition: input.contactPosition || "", contactEmail: input.contactEmail, contactPhone: input.contactPhone || "", + country: input.country || "", isPrimary: input.isPrimary || false, }); + return newContact; }); // 캐시 무효화 revalidateTag(`tech-vendor-contacts-${input.vendorId}`); + revalidateTag("users"); return { data: null, error: null }; } catch (err) { @@ -1245,6 +1249,109 @@ export const findVendorById = async (id: number): Promise<TechVendor | null> => } }; +/* ----------------------------------------------------- + 8) 기술영업 벤더 RFQ 히스토리 조회 +----------------------------------------------------- */ + +/** + * 기술영업 벤더의 RFQ 히스토리 조회 (간단한 버전) + */ +export async function getTechVendorRfqHistory(input: GetTechVendorRfqHistorySchema, id:number) { + try { + + // 먼저 해당 벤더의 견적서가 있는지 확인 + const { techSalesVendorQuotations } = await import("@/db/schema/techSales"); + + const quotationCheck = await db + .select({ count: sql<number>`count(*)`.as("count") }) + .from(techSalesVendorQuotations) + .where(eq(techSalesVendorQuotations.vendorId, id)); + + console.log(`벤더 ${id}의 견적서 개수:`, quotationCheck[0]?.count); + + if (quotationCheck[0]?.count === 0) { + console.log("해당 벤더의 견적서가 없습니다."); + return { data: [], pageCount: 0 }; + } + + const offset = (input.page - 1) * input.perPage; + const { techSalesRfqs } = await import("@/db/schema/techSales"); + const { biddingProjects } = await import("@/db/schema/projects"); + + // 간단한 조회 + let whereCondition = eq(techSalesVendorQuotations.vendorId, id); + + // 검색이 있다면 추가 + if (input.search) { + const s = `%${input.search}%`; + const searchCondition = and( + whereCondition, + or( + ilike(techSalesRfqs.rfqCode, s), + ilike(techSalesRfqs.description, s), + ilike(biddingProjects.pspid, s), + ilike(biddingProjects.projNm, s) + ) + ); + whereCondition = searchCondition; + } + + // 데이터 조회 - 테이블에 필요한 필드들 (프로젝트 타입 추가) + const data = await db + .select({ + id: techSalesRfqs.id, + rfqCode: techSalesRfqs.rfqCode, + description: techSalesRfqs.description, + projectCode: biddingProjects.pspid, + projectName: biddingProjects.projNm, + projectType: biddingProjects.pjtType, // 프로젝트 타입 추가 + status: techSalesRfqs.status, + totalAmount: techSalesVendorQuotations.totalPrice, + currency: techSalesVendorQuotations.currency, + dueDate: techSalesRfqs.dueDate, + createdAt: techSalesRfqs.createdAt, + quotationCode: techSalesVendorQuotations.quotationCode, + submittedAt: techSalesVendorQuotations.submittedAt, + }) + .from(techSalesVendorQuotations) + .innerJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id)) + .leftJoin(biddingProjects, eq(techSalesRfqs.biddingProjectId, biddingProjects.id)) + .where(whereCondition) + .orderBy(desc(techSalesRfqs.createdAt)) + .limit(input.perPage) + .offset(offset); + + console.log("조회된 데이터:", data.length, "개"); + + // 전체 개수 조회 + const totalResult = await db + .select({ count: sql<number>`count(*)`.as("count") }) + .from(techSalesVendorQuotations) + .innerJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id)) + .leftJoin(biddingProjects, eq(techSalesRfqs.biddingProjectId, biddingProjects.id)) + .where(whereCondition); + + const total = totalResult[0]?.count || 0; + const pageCount = Math.ceil(total / input.perPage); + + console.log("기술영업 벤더 RFQ 히스토리 조회 완료", { + id, + dataLength: data.length, + total, + pageCount + }); + + return { data, pageCount }; + } catch (err) { + console.error("기술영업 벤더 RFQ 히스토리 조회 오류:", { + err, + id, + stack: err instanceof Error ? err.stack : undefined + }); + return { data: [], pageCount: 0 }; + } +} + /** * 기술영업 벤더 엑셀 import 시 유저 생성 및 아이템 등록 */ @@ -1408,43 +1515,91 @@ export async function createTechVendorFromSignup(params: { try { console.log("기술영업 벤더 회원가입 시작:", params.vendorData.vendorName); + // 초대 토큰 검증 + let existingVendorId: number | null = null; + if (params.invitationToken) { + const { verifyTechVendorInvitationToken } = await import("@/lib/tech-vendor-invitation-token"); + const tokenPayload = await verifyTechVendorInvitationToken(params.invitationToken); + + if (!tokenPayload) { + throw new Error("유효하지 않은 초대 토큰입니다."); + } + + existingVendorId = tokenPayload.vendorId; + console.log("초대 토큰 검증 성공, 벤더 ID:", existingVendorId); + } + const result = await db.transaction(async (tx) => { - // 1. 이메일 중복 체크 - const existingVendor = await tx.query.techVendors.findFirst({ - where: eq(techVendors.email, params.vendorData.email), - columns: { id: true, vendorName: true } - }); + let vendorResult; + + if (existingVendorId) { + // 기존 벤더 정보 업데이트 + const [updatedVendor] = await tx.update(techVendors) + .set({ + vendorName: params.vendorData.vendorName, + vendorCode: params.vendorData.vendorCode || null, + taxId: params.vendorData.taxId, + country: params.vendorData.country, + address: params.vendorData.address || null, + phone: params.vendorData.phone || null, + email: params.vendorData.email, + website: params.vendorData.website || null, + techVendorType: params.vendorData.techVendorType, + status: "QUOTE_COMPARISON", // 가입 완료 시 QUOTE_COMPARISON으로 변경 + representativeName: params.vendorData.representativeName || null, + representativeEmail: params.vendorData.representativeEmail || null, + representativePhone: params.vendorData.representativePhone || null, + representativeBirth: params.vendorData.representativeBirth || null, + items: params.vendorData.items, + updatedAt: new Date(), + }) + .where(eq(techVendors.id, existingVendorId)) + .returning(); + + vendorResult = updatedVendor; + console.log("기존 벤더 정보 업데이트 완료:", vendorResult.id); + } else { + // 1. 이메일 중복 체크 (새 벤더인 경우) + const existingVendor = await tx.query.techVendors.findFirst({ + where: eq(techVendors.email, params.vendorData.email), + columns: { id: true, vendorName: true } + }); - if (existingVendor) { - throw new Error(`이미 등록된 이메일입니다: ${params.vendorData.email}`); - } + if (existingVendor) { + throw new Error(`이미 등록된 이메일입니다: ${params.vendorData.email}`); + } - // 2. 벤더 생성 - const [newVendor] = await tx.insert(techVendors).values({ - vendorName: params.vendorData.vendorName, - vendorCode: params.vendorData.vendorCode || null, - taxId: params.vendorData.taxId, - country: params.vendorData.country, - address: params.vendorData.address || null, - phone: params.vendorData.phone || null, - email: params.vendorData.email, - website: params.vendorData.website || null, - techVendorType: params.vendorData.techVendorType, - status: "ACTIVE", - representativeName: params.vendorData.representativeName || null, - representativeEmail: params.vendorData.representativeEmail || null, - representativePhone: params.vendorData.representativePhone || null, - representativeBirth: params.vendorData.representativeBirth || null, - items: params.vendorData.items, - }).returning(); + // 2. 새 벤더 생성 + const [newVendor] = await tx.insert(techVendors).values({ + vendorName: params.vendorData.vendorName, + vendorCode: params.vendorData.vendorCode || null, + taxId: params.vendorData.taxId, + country: params.vendorData.country, + address: params.vendorData.address || null, + phone: params.vendorData.phone || null, + email: params.vendorData.email, + website: params.vendorData.website || null, + techVendorType: params.vendorData.techVendorType, + status: "ACTIVE", + isQuoteComparison: false, + representativeName: params.vendorData.representativeName || null, + representativeEmail: params.vendorData.representativeEmail || null, + representativePhone: params.vendorData.representativePhone || null, + representativeBirth: params.vendorData.representativeBirth || null, + items: params.vendorData.items, + }).returning(); + + vendorResult = newVendor; + console.log("새 벤더 생성 완료:", vendorResult.id); + } - console.log("기술영업 벤더 생성 성공:", newVendor.id); + // 이 부분은 위에서 이미 처리되었으므로 주석 처리 // 3. 연락처 생성 if (params.contacts && params.contacts.length > 0) { for (const [index, contact] of params.contacts.entries()) { await tx.insert(techVendorContacts).values({ - vendorId: newVendor.id, + vendorId: vendorResult.id, contactName: contact.contactName, contactPosition: contact.contactPosition || null, contactEmail: contact.contactEmail, @@ -1457,7 +1612,7 @@ export async function createTechVendorFromSignup(params: { // 4. 첨부파일 처리 if (params.files && params.files.length > 0) { - await storeTechVendorFiles(tx, newVendor.id, params.files, "GENERAL"); + await storeTechVendorFiles(tx, vendorResult.id, params.files, "GENERAL"); console.log("첨부파일 저장 완료:", params.files.length, "개"); } @@ -1474,7 +1629,7 @@ export async function createTechVendorFromSignup(params: { const [newUser] = await tx.insert(users).values({ name: params.vendorData.vendorName, email: params.vendorData.email, - techCompanyId: newVendor.id, // 중요: techCompanyId 설정 + techCompanyId: vendorResult.id, // 중요: techCompanyId 설정 domain: "partners", }).returning(); userId = newUser.id; @@ -1483,7 +1638,7 @@ export async function createTechVendorFromSignup(params: { // 기존 유저의 techCompanyId 업데이트 if (!existingUser.techCompanyId) { await tx.update(users) - .set({ techCompanyId: newVendor.id }) + .set({ techCompanyId: vendorResult.id }) .where(eq(users.id, existingUser.id)); console.log("기존 유저의 techCompanyId 업데이트:", existingUser.id); } @@ -1494,13 +1649,13 @@ export async function createTechVendorFromSignup(params: { if (params.vendorData.email) { await tx.update(techVendorCandidates) .set({ - vendorId: newVendor.id, + vendorId: vendorResult.id, status: "INVITED" }) .where(eq(techVendorCandidates.contactEmail, params.vendorData.email)); } - return { vendor: newVendor, userId }; + return { vendor: vendorResult, userId }; }); // 캐시 무효화 @@ -1538,6 +1693,7 @@ export async function addTechVendor(input: { representativeEmail?: string | null; representativePhone?: string | null; representativeBirth?: string | null; + isQuoteComparison?: boolean; }) { unstable_noStore(); @@ -1565,7 +1721,7 @@ export async function addTechVendor(input: { const [newVendor] = await tx.insert(techVendors).values({ vendorName: input.vendorName, vendorCode: input.vendorCode || null, - taxId: input.taxId, + taxId: input.taxId || null, country: input.country || null, countryEng: input.countryEng || null, countryFab: input.countryFab || null, @@ -1577,7 +1733,8 @@ export async function addTechVendor(input: { email: input.email, website: input.website || null, techVendorType: Array.isArray(input.techVendorType) ? input.techVendorType.join(',') : input.techVendorType, - status: "ACTIVE", + status: input.isQuoteComparison ? "PENDING_INVITE" : "ACTIVE", + isQuoteComparison: input.isQuoteComparison || false, representativeName: input.representativeName || null, representativeEmail: input.representativeEmail || null, representativePhone: input.representativePhone || null, @@ -1586,7 +1743,11 @@ export async function addTechVendor(input: { console.log("벤더 생성 성공:", newVendor.id); - // 3. 유저 생성 (techCompanyId 설정) + // 3. 견적비교용 벤더인 경우 PENDING_REVIEW 상태로 생성됨 + // 초대는 별도의 초대 버튼을 통해 진행 + console.log("벤더 생성 완료:", newVendor.id, "상태:", newVendor.status); + + // 4. 유저 생성 (techCompanyId 설정) console.log("유저 생성 시도:", input.email); // 이미 존재하는 유저인지 확인 @@ -1650,3 +1811,80 @@ export async function getTechVendorPossibleItemsCount(vendorId: number): Promise } } +/** + * 기술영업 벤더 초대 메일 발송 + */ +export async function inviteTechVendor(params: { + vendorId: number; + subject: string; + message: string; + recipientEmail: string; +}) { + unstable_noStore(); + + try { + console.log("기술영업 벤더 초대 메일 발송 시작:", params.vendorId); + + const result = await db.transaction(async (tx) => { + // 벤더 정보 조회 + const vendor = await tx.query.techVendors.findFirst({ + where: eq(techVendors.id, params.vendorId), + }); + + if (!vendor) { + throw new Error("벤더를 찾을 수 없습니다."); + } + + // 벤더 상태를 INVITED로 변경 (PENDING_INVITE에서) + if (vendor.status !== "PENDING_INVITE") { + throw new Error("초대 가능한 상태가 아닙니다. (PENDING_INVITE 상태만 초대 가능)"); + } + + await tx.update(techVendors) + .set({ + status: "INVITED", + updatedAt: new Date(), + }) + .where(eq(techVendors.id, params.vendorId)); + + // 초대 토큰 생성 + const { createTechVendorInvitationToken, createTechVendorSignupUrl } = await import("@/lib/tech-vendor-invitation-token"); + const { sendEmail } = await import("@/lib/mail/sendEmail"); + + const invitationToken = await createTechVendorInvitationToken({ + vendorId: vendor.id, + vendorName: vendor.vendorName, + email: params.recipientEmail, + }); + + const signupUrl = await createTechVendorSignupUrl(invitationToken); + + // 초대 메일 발송 + await sendEmail({ + to: params.recipientEmail, + subject: params.subject, + template: "tech-vendor-invitation", + context: { + companyName: vendor.vendorName, + language: "ko", + registrationLink: signupUrl, + customMessage: params.message, + } + }); + + console.log("초대 메일 발송 완료:", params.recipientEmail); + + return { vendor, invitationToken, signupUrl }; + }); + + // 캐시 무효화 + revalidateTag("tech-vendors"); + + console.log("기술영업 벤더 초대 완료:", result); + return { success: true, data: result }; + } catch (error) { + console.error("기술영업 벤더 초대 실패:", error); + return { success: false, error: getErrorMessage(error) }; + } +} + |
