import { createSearchParamsCache, parseAsArrayOf, parseAsInteger, parseAsString, parseAsStringEnum, } from "nuqs/server" 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" 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(""), }); const creditAgencyEnum = z.enum(["NICE", "KIS", "KED", "SCI"]); export type CreditAgencyType = z.infer; export const updateVendorSchema = z.object({ vendorName: z.string().min(1, "업체명은 필수 입력사항입니다"), vendorCode: z.string().optional(), address: z.string().optional(), country: z.string().optional(), phone: z.string().optional(), email: z.string().email("유효한 이메일 주소를 입력해주세요").optional(), website: z.string().url("유효한 URL을 입력해주세요").optional(), status: z.enum(vendors.status.enumValues).optional(), vendorTypeId: z.number().optional(), // Optional fields for buyer information buyerName: z.string().optional(), buyerDepartment: z.string().optional(), contractStartDate: z.date().optional(), contractEndDate: z.date().optional(), internalNotes: z.string().optional(), creditRating: z.string().optional(), cashFlowRating: z.string().optional(), creditAgency: creditAgencyEnum.optional(), // evaluationScore: z.string().optional(), }); 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"), vendorTypeId: z.number({ required_error: "업체유형을 선택해주세요" }), email: z.string().email("Invalid email").max(255), // 나머지 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("유효하지 않은 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: "첨부 파일은 필수입니다." } ), 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."), // ... (기타 필드) }) .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) 업체일 경우 필수입니다.", }) } } } ) 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> export const updateVendorInfoSchema = z.object({ vendorName: z.string().min(1, "업체명은 필수 입력사항입니다."), taxId: z.string(), address: z.string().optional(), country: z.string().min(1, "국가를 선택해 주세요."), phone: z.string().optional(), email: z.string().email("유효한 이메일을 입력해 주세요."), website: z.string().optional(), // 한국 사업자 정보 (KR일 경우 필수 항목들) representativeName: z.string().optional(), representativeBirth: z.string().optional(), representativeEmail: z.string().optional(), representativePhone: z.string().optional(), corporateRegistrationNumber: z.string().optional(), // 신용평가 정보 creditAgency: z.string().optional(), creditRating: z.string().optional(), cashFlowRating: z.string().optional(), // 첨부파일 attachedFiles: z.any().optional(), creditRatingAttachment: z.any().optional(), cashFlowRatingAttachment: z.any().optional(), // 연락처 정보 contacts: z.array(contactSchema).min(1, "최소 1명의 담당자가 필요합니다."), }) export const updateVendorSchemaWithConditions = updateVendorInfoSchema.superRefine( (data, ctx) => { // 국가가 한국(KR)인 경우, 한국 사업자 정보 필수 if (data.country === "KR") { if (!data.representativeName) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "대표자 이름은 필수 입력사항입니다.", path: ["representativeName"], }) } if (!data.representativeBirth) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "대표자 생년월일은 필수 입력사항입니다.", path: ["representativeBirth"], }) } if (!data.representativeEmail) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "대표자 이메일은 필수 입력사항입니다.", path: ["representativeEmail"], }) } if (!data.representativePhone) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "대표자 전화번호는 필수 입력사항입니다.", path: ["representativePhone"], }) } if (!data.corporateRegistrationNumber) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "법인등록번호는 필수 입력사항입니다.", path: ["corporateRegistrationNumber"], }) } // 신용평가사가 선택된 경우, 등급 정보 필수 if (data.creditAgency) { if (!data.creditRating) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "신용평가등급은 필수 입력사항입니다.", path: ["creditRating"], }) } if (!data.cashFlowRating) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "현금흐름등급은 필수 입력사항입니다.", path: ["cashFlowRating"], }) } } } } ) export type UpdateVendorInfoSchema = z.infer export const searchParamsMaterialCache = 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 type GetVendorMaterialsSchema = Awaited>