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().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().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().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().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> export type GetVendorContactsSchema = Awaited> export type GetVendorItemsSchema = Awaited> export type UpdateVendorSchema = z.infer export type CreateVendorSchema = z.infer export type CreateVendorContactSchema = z.infer export type UpdateVendorContactSchema = z.infer export type CreateVendorItemSchema = z.infer export type UpdateVendorItemSchema = z.infer export type GetRfqHistorySchema = Awaited>