diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
| commit | e9897d416b3e7327bbd4d4aef887eee37751ae82 (patch) | |
| tree | bd20ce6eadf9b21755bd7425492d2d31c7700a0e /lib/vendors | |
| parent | 3bf1952c1dad9d479bb8b22031b06a7434d37c37 (diff) | |
(대표님) 20250627 오전 10시 작업사항
Diffstat (limited to 'lib/vendors')
| -rw-r--r-- | lib/vendors/repository.ts | 2 | ||||
| -rw-r--r-- | lib/vendors/service.ts | 45 | ||||
| -rw-r--r-- | lib/vendors/validations.ts | 146 |
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() |
