summaryrefslogtreecommitdiff
path: root/lib/vendors/validations.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-03-25 15:55:45 +0900
committerjoonhoekim <26rote@gmail.com>2025-03-25 15:55:45 +0900
commit1a2241c40e10193c5ff7008a7b7b36cc1d855d96 (patch)
tree8a5587f10ca55b162d7e3254cb088b323a34c41b /lib/vendors/validations.ts
initial commit
Diffstat (limited to 'lib/vendors/validations.ts')
-rw-r--r--lib/vendors/validations.ts341
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>>