summaryrefslogtreecommitdiff
path: root/lib/tech-vendors/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tech-vendors/service.ts')
-rw-r--r--lib/tech-vendors/service.ts312
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) };
+ }
+}
+