diff options
Diffstat (limited to 'lib/vendors/validations.ts')
| -rw-r--r-- | lib/vendors/validations.ts | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/lib/vendors/validations.ts b/lib/vendors/validations.ts new file mode 100644 index 00000000..14efc8dc --- /dev/null +++ b/lib/vendors/validations.ts @@ -0,0 +1,341 @@ +import { tasks, type Task } from "@/db/schema/tasks"; +import { + createSearchParamsCache, + parseAsArrayOf, + parseAsInteger, + parseAsString, + parseAsStringEnum, +} from "nuqs/server" +import * as z from "zod" + +import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" +import { Vendor, VendorContact, VendorItemsView, vendors } from "@/db/schema/vendors"; +import { rfqs } from "@/db/schema/rfq" + + +export const searchParamsCache = createSearchParamsCache({ + + // 공통 플래그 + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault( + [] + ), + + // 페이징 + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + + // 정렬 (vendors 테이블에 맞춰 Vendor 타입 지정) + sort: getSortingStateParser<Vendor>().withDefault([ + { id: "createdAt", desc: true }, // createdAt 기준 내림차순 + ]), + + // 고급 필터 + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + + // 검색 키워드 + search: parseAsString.withDefault(""), + + // ----------------------------------------------------------------- + // 여기부터는 "벤더"에 특화된 검색 필드 예시 + // ----------------------------------------------------------------- + // 상태 (ACTIVE, INACTIVE, BLACKLISTED 등) 중에서 선택 + status: parseAsStringEnum(["ACTIVE", "INACTIVE", "BLACKLISTED"]), + + // 벤더명 검색 + vendorName: parseAsString.withDefault(""), + + // 국가 검색 + country: parseAsString.withDefault(""), + + // 예) 코드 검색 + vendorCode: parseAsString.withDefault(""), + + // 필요하다면 이메일 검색 / 웹사이트 검색 등 추가 가능 + email: parseAsString.withDefault(""), + website: parseAsString.withDefault(""), +}); + +export const searchParamsContactCache = createSearchParamsCache({ + // 공통 플래그 + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault( + [] + ), + + // 페이징 + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + + // 정렬 (vendors 테이블에 맞춰 Vendor 타입 지정) + sort: getSortingStateParser<VendorContact>().withDefault([ + { id: "createdAt", desc: true }, // createdAt 기준 내림차순 + ]), + + // 고급 필터 + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + + // 검색 키워드 + search: parseAsString.withDefault(""), + + + contactName: parseAsString.withDefault(""), + contactPosition: parseAsString.withDefault(""), + contactEmail: parseAsString.withDefault(""), + contactPhone: parseAsString.withDefault(""), +}); + + + +export const searchParamsItemCache = createSearchParamsCache({ + // 공통 플래그 + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault( + [] + ), + + // 페이징 + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + + // 정렬 (vendors 테이블에 맞춰 Vendor 타입 지정) + sort: getSortingStateParser<VendorItemsView>().withDefault([ + { id: "createdAt", desc: true }, // createdAt 기준 내림차순 + ]), + + // 고급 필터 + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + + // 검색 키워드 + search: parseAsString.withDefault(""), + + + itemName: parseAsString.withDefault(""), + itemCode: parseAsString.withDefault(""), + description: parseAsString.withDefault(""), +}); + + +export const updateVendorSchema = z.object({ + vendorName: z.string().min(1, "Vendor name is required").max(255, "Max length 255").optional(), + vendorCode: z.string().max(100, "Max length 100").optional(), + address: z.string().optional(), + country: z.string().max(100, "Max length 100").optional(), + phone: z.string().max(50, "Max length 50").optional(), + email: z.string().email("Invalid email").max(255).optional(), + website: z.string().url("Invalid URL").max(255).optional(), + + // status는 특정 값만 허용하도록 enum 사용 예시 + // 필요 시 'SUSPENDED', 'BLACKLISTED' 등 추가하거나 제거 가능 + status: z.enum(vendors.status.enumValues) + .optional() + .default("ACTIVE"), +}); + + +const contactSchema = z.object({ + contactName: z + .string() + .min(1, "Contact name is required") + .max(255, "Max length 255"), + contactPosition: z.string().max(100).optional(), + contactEmail: z.string().email("Invalid email").max(255), + contactPhone: z.string().max(50).optional(), + isPrimary: z.boolean().default(false).optional()}) + +const vendorStatusEnum = z.enum(vendors.status.enumValues) +// CREATE 시: 일부 필드는 필수, 일부는 optional +export const createVendorSchema = z + .object({ + + vendorName: z + .string() + .min(1, "Vendor name is required") + .max(255, "Max length 255"), + email: z.string().email("Invalid email").max(255), + taxId: z.string().max(100, "Max length 100"), + + // 나머지 optional + 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(), + website: z.string().url("Invalid URL").max(255).optional(), + + creditRatingAttachment: z.any().optional(), // 신용평가 첨부 + cashFlowRatingAttachment: z.any().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: "첨부 파일은 필수입니다." } + ), + 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(), + + creditAgency: z.string().max(50).optional(), + creditRating: z.string().max(50).optional(), + cashFlowRating: z.string().max(50).optional(), + + contacts: z + .array(contactSchema) + .nonempty("At least one contact is required."), + + // ... (기타 필드) + }) + .superRefine((data, ctx) => { + 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) 업체일 경우 필수입니다.", + }) + } + if (!data.corporateRegistrationNumber) { + ctx.addIssue({ + code: "custom", + path: ["corporateRegistrationNumber"], + message: "법인등록번호는 한국(KR) 업체일 경우 필수입니다.", + }) + } + + // 2) 신용/현금흐름 등급도 필수라면 + if (!data.creditAgency) { + ctx.addIssue({ + code: "custom", + path: ["creditAgency"], + message: "신용평가사 선택은 한국(KR) 업체일 경우 필수입니다.", + }) + } + if (!data.creditRating) { + ctx.addIssue({ + code: "custom", + path: ["creditRating"], + message: "신용평가등급은 한국(KR) 업체일 경우 필수입니다.", + }) + } + if (!data.cashFlowRating) { + ctx.addIssue({ + code: "custom", + path: ["cashFlowRating"], + message: "현금흐름등급은 한국(KR) 업체일 경우 필수입니다.", + }) + } + } + } +) + +export const createVendorContactSchema = z.object({ + vendorId: z.number(), + contactName: z.string() + .min(1, "Contact name is required") + .max(255, "Max length 255"), // 신규 생성 시 반드시 입력 + contactPosition: z.string().max(100, "Max length 100"), + contactEmail: z.string().email(), + contactPhone: z.string().max(50, "Max length 50").optional(), + isPrimary: z.boolean(), +}); + + +export const updateVendorContactSchema = z.object({ + contactName: z.string() + .min(1, "Contact name is required") + .max(255, "Max length 255"), // 신규 생성 시 반드시 입력 + contactPosition: z.string().max(100, "Max length 100").optional(), + contactEmail: z.string().email().optional(), + contactPhone: z.string().max(50, "Max length 50").optional(), + isPrimary: z.boolean().optional(), +}); + + + +export const createVendorItemSchema = z.object({ + vendorId: z.number(), + itemCode: z.string().max(100, "Max length 100"), + +}); + + +export const updateVendorItemSchema = z.object({ + itemName: z.string().optional(), + itemCode: z.string().max(100, "Max length 100"), + description: z.string().optional() +}); + +export const searchParamsRfqHistoryCache = createSearchParamsCache({ + // 공통 플래그 + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault( + [] + ), + + // 페이징 + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + + // 정렬 + sort: getSortingStateParser<typeof rfqs.$inferSelect>().withDefault([ + { id: "createdAt", desc: true }, + ]), + + // 고급 필터 + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + + // 검색 키워드 + search: parseAsString.withDefault(""), + + // RFQ 특화 필터 + rfqCode: parseAsString.withDefault(""), + projectCode: parseAsString.withDefault(""), + projectName: parseAsString.withDefault(""), + status: parseAsStringEnum(["DRAFT", "IN_PROGRESS", "COMPLETED", "CANCELLED"]), + vendorStatus: parseAsStringEnum(["INVITED", "ACCEPTED", "DECLINED", "SUBMITTED", "AWARDED", "REJECTED"]), + dueDate: parseAsString.withDefault(""), +}); + +export type GetVendorsSchema = Awaited<ReturnType<typeof searchParamsCache.parse>> +export type GetVendorContactsSchema = Awaited<ReturnType<typeof searchParamsContactCache.parse>> +export type GetVendorItemsSchema = Awaited<ReturnType<typeof searchParamsItemCache.parse>> + +export type UpdateVendorSchema = z.infer<typeof updateVendorSchema> +export type CreateVendorSchema = z.infer<typeof createVendorSchema> +export type CreateVendorContactSchema = z.infer<typeof createVendorContactSchema> +export type UpdateVendorContactSchema = z.infer<typeof updateVendorContactSchema> +export type CreateVendorItemSchema = z.infer<typeof createVendorItemSchema> +export type UpdateVendorItemSchema = z.infer<typeof updateVendorItemSchema> +export type GetRfqHistorySchema = Awaited<ReturnType<typeof searchParamsRfqHistoryCache.parse>> |
