summaryrefslogtreecommitdiff
path: root/lib/vendors
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-27 01:16:20 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-27 01:16:20 +0000
commite9897d416b3e7327bbd4d4aef887eee37751ae82 (patch)
treebd20ce6eadf9b21755bd7425492d2d31c7700a0e /lib/vendors
parent3bf1952c1dad9d479bb8b22031b06a7434d37c37 (diff)
(대표님) 20250627 오전 10시 작업사항
Diffstat (limited to 'lib/vendors')
-rw-r--r--lib/vendors/repository.ts2
-rw-r--r--lib/vendors/service.ts45
-rw-r--r--lib/vendors/validations.ts146
3 files changed, 152 insertions, 41 deletions
diff --git a/lib/vendors/repository.ts b/lib/vendors/repository.ts
index 04d6322a..41ac468b 100644
--- a/lib/vendors/repository.ts
+++ b/lib/vendors/repository.ts
@@ -79,7 +79,7 @@ export async function countVendorsWithTypes(
*/
export async function insertVendor(
tx: PgTransaction<any, any, any>,
- data: Omit<Vendor, "id" | "createdAt" | "updatedAt">
+ data: Omit<Vendor, "id" | "createdAt" | "updatedAt" | "businessSize">
) {
return tx.insert(vendors).values(data).returning();
}
diff --git a/lib/vendors/service.ts b/lib/vendors/service.ts
index fb834814..7c6ac15d 100644
--- a/lib/vendors/service.ts
+++ b/lib/vendors/service.ts
@@ -30,6 +30,7 @@ import {
selectVendorsWithTypes,
countVendorsWithTypes,
countVendorMaterials,
+ selectVendorMaterials,
insertVendorMaterial,
} from "./repository";
@@ -56,7 +57,7 @@ import { promises as fsPromises } from 'fs';
import { sendEmail } from "../mail/sendEmail";
import { PgTransaction } from "drizzle-orm/pg-core";
import { items, materials } from "@/db/schema/items";
-import { users } from "@/db/schema/users";
+import { roles, userRoles, users } from "@/db/schema/users";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { contracts, contractsDetailView, projects, vendorPQSubmissions, vendorProjectPQs, vendorsLogs } from "@/db/schema";
@@ -306,7 +307,8 @@ export type CreateVendorData = {
creditRating?: string
cashFlowRating?: string
corporateRegistrationNumber?: string
-
+ businessSize?: string
+
country?: string
status?: "PENDING_REVIEW" | "IN_REVIEW" | "IN_PQ" | "PQ_FAILED" | "APPROVED" | "ACTIVE" | "INACTIVE" | "BLACKLISTED" | "PQ_SUBMITTED"
}
@@ -1468,7 +1470,7 @@ interface ApproveVendorsInput {
*/
export async function approveVendors(input: ApproveVendorsInput & { userId: number }) {
unstable_noStore();
-
+
try {
// 트랜잭션 내에서 협력업체 상태 업데이트, 유저 생성 및 이메일 발송
const result = await db.transaction(async (tx) => {
@@ -1507,7 +1509,7 @@ export async function approveVendors(input: ApproveVendorsInput & { userId: numb
if (!vendor.email) return; // 이메일이 없으면 스킵
// 이미 존재하는 유저인지 확인
- const existingUser = await db.query.users.findFirst({
+ const existingUser = await tx.query.users.findFirst({
where: eq(users.email, vendor.email),
columns: {
id: true
@@ -1516,11 +1518,42 @@ export async function approveVendors(input: ApproveVendorsInput & { userId: numb
// 유저가 존재하지 않는 경우에만 생성
if (!existingUser) {
- await tx.insert(users).values({
+ // 유저 생성
+ const [newUser] = await tx.insert(users).values({
name: vendor.vendorName,
email: vendor.email,
companyId: vendor.id,
domain: "partners", // 기본값으로 이미 설정되어 있지만 명시적으로 지정
+ }).returning({ id: users.id });
+
+ // "Vendor Admin" 역할 찾기 또는 생성
+ let vendorAdminRole = await tx.query.roles.findFirst({
+ where: and(
+ eq(roles.name, "Vendor Admin"),
+ eq(roles.domain, "partners"),
+ eq(roles.companyId, vendor.id)
+ ),
+ columns: {
+ id: true
+ }
+ });
+
+ // "Vendor Admin" 역할이 없다면 생성
+ if (!vendorAdminRole) {
+ const [newRole] = await tx.insert(roles).values({
+ name: "Vendor Admin",
+ domain: "partners",
+ companyId: vendor.id,
+ description: "Vendor Administrator role",
+ }).returning({ id: roles.id });
+
+ vendorAdminRole = newRole;
+ }
+
+ // userRoles 테이블에 관계 생성
+ await tx.insert(userRoles).values({
+ userId: newUser.id,
+ roleId: vendorAdminRole.id,
});
}
})
@@ -1580,6 +1613,8 @@ export async function approveVendors(input: ApproveVendorsInput & { userId: numb
revalidateTag("vendors");
revalidateTag("vendor-status-counts");
revalidateTag("users"); // 유저 캐시도 무효화
+ revalidateTag("roles"); // 역할 캐시도 무효화
+ revalidateTag("user-roles"); // 유저 역할 캐시도 무효화
return { data: result, error: null };
} catch (err) {
diff --git a/lib/vendors/validations.ts b/lib/vendors/validations.ts
index 7ba54ccf..07eaae83 100644
--- a/lib/vendors/validations.ts
+++ b/lib/vendors/validations.ts
@@ -10,6 +10,7 @@ import * as z from "zod"
import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
import { Vendor, VendorContact, VendorItemsView, VendorMaterialsView, vendors, VendorWithType } from "@/db/schema/vendors";
import { rfqs } from "@/db/schema/rfq"
+import { countryDialCodes } from "@/components/signup/join-form";
export const searchParamsCache = createSearchParamsCache({
@@ -155,97 +156,172 @@ const contactSchema = z.object({
const vendorStatusEnum = z.enum(vendors.status.enumValues)
// CREATE 시: 일부 필드는 필수, 일부는 optional
+// Phone validation helper function
+const validatePhoneByCountry = (phone: string, country: string): boolean => {
+ if (!phone || !country) return false;
+
+ // Remove spaces, hyphens, and parentheses for validation
+ const cleanPhone = phone.replace(/[\s\-\(\)]/g, '');
+
+ switch (country) {
+ case 'KR': // South Korea
+ // Should start with +82 and have 10-11 digits after country code
+ return /^\+82[1-9]\d{7,9}$/.test(cleanPhone) || /^0[1-9]\d{7,9}$/.test(cleanPhone);
+
+ case 'US': // United States
+ case 'CA': // Canada
+ // Should start with +1 and have exactly 10 digits after
+ return /^\+1[2-9]\d{9}$/.test(cleanPhone);
+
+ case 'JP': // Japan
+ // Should start with +81 and have 10-11 digits after country code
+ return /^\+81[1-9]\d{8,9}$/.test(cleanPhone);
+
+ case 'CN': // China
+ // Should start with +86 and have 11 digits after country code
+ return /^\+86[1-9]\d{9}$/.test(cleanPhone);
+
+ case 'GB': // United Kingdom
+ // Should start with +44 and have 10-11 digits after country code
+ return /^\+44[1-9]\d{8,9}$/.test(cleanPhone);
+
+ case 'DE': // Germany
+ // Should start with +49 and have 10-12 digits after country code
+ return /^\+49[1-9]\d{9,11}$/.test(cleanPhone);
+
+ case 'FR': // France
+ // Should start with +33 and have 9 digits after country code
+ return /^\+33[1-9]\d{8}$/.test(cleanPhone);
+
+ default:
+ // For other countries, just check if it starts with + and has reasonable length
+ return /^\+\d{10,15}$/.test(cleanPhone);
+ }
+};
+
+// Enhanced createVendorSchema with phone validation
export const createVendorSchema = z
.object({
-
vendorName: z
.string()
.min(1, "Vendor name is required")
.max(255, "Max length 255"),
- vendorTypeId: z.number({ required_error: "업체유형을 선택해주세요" }),
-
+
+ vendorTypeId: z.number({ required_error: "업체유형을 선택해주세요" }),
+
email: z.string().email("Invalid email").max(255),
- // 나머지 optional
+
+ // Updated phone validation - now required and must include country code
+ phone: z.string()
+ .min(1, "전화번호는 필수입니다")
+ .max(50, "Max length 50"),
+
+ // Other fields remain the same
vendorCode: z.string().max(100, "Max length 100").optional(),
address: z.string().optional(),
country: z.string()
- .min(1, "국가 선택은 필수입니다.")
- .max(100, "Max length 100"),
- phone: z.string().max(50, "Max length 50").optional(),
+ .min(1, "국가 선택은 필수입니다.")
+ .max(100, "Max length 100"),
website: z.string().url("유효하지 않은 URL입니다. https:// 혹은 http:// 로 시작하는 주소를 입력해주세요.").max(255).optional(),
-
+
attachedFiles: z.any()
- .refine(
- val => {
- // Validate that files exist and there's at least one file
- return val &&
- (Array.isArray(val) ? val.length > 0 :
- val instanceof FileList ? val.length > 0 :
- val && typeof val === 'object' && 'length' in val && val.length > 0);
- },
- { message: "첨부 파일은 필수입니다." }
- ),
+ .refine(
+ val => {
+ return val &&
+ (Array.isArray(val) ? val.length > 0 :
+ val instanceof FileList ? val.length > 0 :
+ val && typeof val === 'object' && 'length' in val && val.length > 0);
+ },
+ { message: "첨부 파일은 필수입니다." }
+ ),
+
status: vendorStatusEnum.default("PENDING_REVIEW"),
-
+
representativeName: z.union([z.string().max(255), z.literal("")]).optional(),
representativeBirth: z.union([z.string().max(20), z.literal("")]).optional(),
representativeEmail: z.union([z.string().email("Invalid email").max(255), z.literal("")]).optional(),
representativePhone: z.union([z.string().max(50), z.literal("")]).optional(),
corporateRegistrationNumber: z.union([z.string().max(100), z.literal("")]).optional(),
taxId: z.string().min(1, { message: "사업자등록번호를 입력해주세요" }),
-
+
items: z.string().min(1, { message: "공급품목을 입력해주세요" }),
-
+
contacts: z
- .array(contactSchema)
- .nonempty("At least one contact is required."),
-
- // ... (기타 필드)
+ .array(contactSchema)
+ .nonempty("At least one contact is required."),
})
.superRefine((data, ctx) => {
+ // Validate main phone number with country code
+ if (data.phone && data.country) {
+ if (!validatePhoneByCountry(data.phone, data.country)) {
+ const countryDialCode = countryDialCodes[data.country] || "+XX";
+ ctx.addIssue({
+ code: "custom",
+ path: ["phone"],
+ message: `올바른 전화번호 형식이 아닙니다. ${countryDialCode}로 시작하는 국제 전화번호를 입력해주세요. (예: ${countryDialCode}XXXXXXXXX)`,
+ });
+ }
+ }
+
+ // Validate representative phone for Korean companies
if (data.country === "KR") {
- // 1) 대표자 정보가 누락되면 각각 에러 발생
if (!data.representativeName) {
ctx.addIssue({
code: "custom",
path: ["representativeName"],
message: "대표자 이름은 한국(KR) 업체일 경우 필수입니다.",
- })
+ });
}
if (!data.representativeBirth) {
ctx.addIssue({
code: "custom",
path: ["representativeBirth"],
message: "대표자 생년월일은 한국(KR) 업체일 경우 필수입니다.",
- })
+ });
}
if (!data.representativeEmail) {
ctx.addIssue({
code: "custom",
path: ["representativeEmail"],
message: "대표자 이메일은 한국(KR) 업체일 경우 필수입니다.",
- })
+ });
}
if (!data.representativePhone) {
ctx.addIssue({
code: "custom",
path: ["representativePhone"],
message: "대표자 전화번호는 한국(KR) 업체일 경우 필수입니다.",
- })
+ });
+ } else if (!validatePhoneByCountry(data.representativePhone, "KR")) {
+ ctx.addIssue({
+ code: "custom",
+ path: ["representativePhone"],
+ message: "올바른 한국 전화번호 형식이 아닙니다. +82로 시작하거나 010으로 시작하는 번호를 입력해주세요.",
+ });
}
if (!data.corporateRegistrationNumber) {
ctx.addIssue({
code: "custom",
path: ["corporateRegistrationNumber"],
message: "법인등록번호는 한국(KR) 업체일 경우 필수입니다.",
- })
+ });
}
-
-
}
- }
-)
+ // Validate contact phone numbers
+ data.contacts?.forEach((contact, index) => {
+ if (contact.contactPhone && data.country) {
+ if (!validatePhoneByCountry(contact.contactPhone, data.country)) {
+ const countryDialCode = countryDialCodes[data.country] || "+XX";
+ ctx.addIssue({
+ code: "custom",
+ path: ["contacts", index, "contactPhone"],
+ message: `올바른 전화번호 형식이 아닙니다. ${countryDialCode}로 시작하는 국제 전화번호를 입력해주세요.`,
+ });
+ }
+ }
+ });
+ });
export const createVendorContactSchema = z.object({
vendorId: z.number(),
contactName: z.string()