import { BiddingListItem, biddings } from "@/db/schema" import { createSearchParamsCache, parseAsArrayOf, parseAsInteger, parseAsString, parseAsStringEnum, } from "nuqs/server" import * as z from "zod" import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" export const searchParamsCache = createSearchParamsCache({ flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), page: parseAsInteger.withDefault(1), perPage: parseAsInteger.withDefault(10), sort: getSortingStateParser().withDefault([ { id: "createdAt", desc: true }, ]), // 기본 필터 biddingNumber: parseAsString.withDefault(""), status: parseAsArrayOf(z.enum(biddings.status.enumValues)).withDefault([]), biddingType: parseAsArrayOf(z.enum(biddings.biddingType.enumValues)).withDefault([]), contractType: parseAsArrayOf(z.enum(biddings.contractType.enumValues)).withDefault([]), purchasingOrganization: parseAsString.withDefault(""), managerName: parseAsString.withDefault(""), // 날짜 필터 preQuoteDateFrom: parseAsString.withDefault(""), preQuoteDateTo: parseAsString.withDefault(""), submissionDateFrom: parseAsString.withDefault(""), submissionDateTo: parseAsString.withDefault(""), createdAtFrom: parseAsString.withDefault(""), createdAtTo: parseAsString.withDefault(""), // 가격 필터 budgetMin: parseAsString.withDefault(""), budgetMax: parseAsString.withDefault(""), // Boolean 필터 hasSpecificationMeeting: parseAsString.withDefault(""), // "true" | "false" | "" hasPrDocument: parseAsString.withDefault(""), // "true" | "false" | "" // 고급 필터 filters: getFiltersStateParser().withDefault([]), joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), search: parseAsString.withDefault(""), }) export const createBiddingSchema = z.object({ // ❌ 제거: biddingNumber (자동 생성) // ❌ 제거: preQuoteDate (나중에 자동 기록) // ❌ 제거: biddingRegistrationDate (시스템에서 자동 기록) revision: z.number().int().min(0).default(0), // ✅ 프로젝트 정보 (새로 추가) - 임시로 optional로 변경 projectId: z.number().optional(), // 임시로 필수 해제 projectName: z.string().optional(), // ProjectSelector에서 자동 설정 // ✅ 필수 필드들 itemName: z.string().optional(), // 임시로 필수 해제 title: z.string().min(1, "입찰명은 필수입니다"), description: z.string().optional(), content: z.string().optional(), // 입찰공고 정보 noticeType: z.enum(['standard', 'facility', 'unit_price'], { required_error: "입찰공고 타입을 선택해주세요" }), // ✅ 계약 정보 (필수) contractType: z.enum(biddings.contractType.enumValues, { required_error: "계약구분을 선택해주세요" }), biddingType: z.enum(biddings.biddingType.enumValues, { required_error: "입찰유형을 선택해주세요" }), biddingTypeCustom: z.string().optional(), awardCount: z.enum(biddings.awardCount.enumValues, { required_error: "낙찰업체 수를 선택해주세요" }), // ✅ 가격 정보 (조회용으로 readonly 처리) budget: z.string().optional(), // 예산 (조회용) finalBidPrice: z.string().optional(), // 실적가 (조회용) targetPrice: z.string().optional(), // 내정가 (조회용) // PR 정보 (조회용) prNumber: z.string().optional(), // 계약기간 contractStartDate: z.string().optional(), contractEndDate: z.string().optional(), // ✅ 일정 (제출기간 필수) submissionStartDate: z.string().optional(), submissionEndDate: z.string().optional(), // 회의 및 문서 hasSpecificationMeeting: z.boolean().default(false), hasPrDocument: z.boolean().default(false), // ✅ 가격 정보 (통화 필수) currency: z.string().min(1, "통화를 선택해주세요").default("KRW"), // 상태 (조회용) status: z.enum(biddings.status.enumValues).default("bidding_generated"), isPublic: z.boolean().default(false), isUrgent: z.boolean().default(false), // 구매조직 purchasingOrganization: z.string().min(1, "구매조직을 선택해주세요"), // 담당자 정보 (개선된 구조) bidPicId: z.number().int().positive().optional(), bidPicName: z.string().min(1, "입찰담당자는 필수입니다"), bidPicCode: z.string().min(1, "입찰담당자 코드는 필수입니다"), supplyPicId: z.number().int().positive().optional(), supplyPicName: z.string().optional(), supplyPicCode: z.string().optional(), // 기존 담당자 정보 (점진적 마이그레이션을 위해 유지) managerName: z.string().optional(), managerEmail: z.string().email().optional().or(z.literal("")), managerPhone: z.string().optional(), // 구매요청자 (현재 사용자) requesterName: z.string().optional(), // 메타 remarks: z.string().optional(), // 첨부파일 (두 가지 타입으로 구분) attachments: z.array(z.object({ id: z.string(), fileName: z.string(), fileSize: z.number().optional(), filePath: z.string(), uploadedAt: z.string().optional(), type: z.enum(['shi', 'vendor']).default('shi'), // SHI용 또는 협력업체용 })).default([]), vendorAttachments: z.array(z.object({ id: z.string(), fileName: z.string(), fileSize: z.number().optional(), filePath: z.string(), uploadedAt: z.string().optional(), type: z.enum(['shi', 'vendor']).default('vendor'), // SHI용 또는 협력업체용 })).default([]), // 입찰 조건 (통합된 구조) biddingConditions: z.object({ paymentTerms: z.string().min(1, "SHI 지급조건은 필수입니다"), // SHI 지급조건 taxConditions: z.string().min(1, "SHI 매입부가가치세는 필수입니다"), // SHI 매입부가가치세 incoterms: z.string().min(1, "SHI 인도조건은 필수입니다"), // SHI 인도조건 incotermsOption: z.string().optional(), // SHI 인도조건2 contractDeliveryDate: z.string().optional(), shippingPort: z.string().optional(), // SHI 선적지 destinationPort: z.string().optional(), // SHI 하역지 isPriceAdjustmentApplicable: z.boolean().default(false), // 하도급법적용여부 sparePartOptions: z.string().optional(), }), }).refine((data) => { // 제출 기간 검증: 시작일이 마감일보다 이전이어야 함 if (data.submissionStartDate && data.submissionEndDate) { const startDate = new Date(data.submissionStartDate) const endDate = new Date(data.submissionEndDate) return startDate < endDate } return true }, { message: "제출시작일시가 제출마감일시보다 늦을 수 없습니다", path: ["submissionEndDate"] }).refine((data) => { // 기타 입찰유형 선택 시 직접입력 필드 검증 if (data.biddingType === "other") { return data.biddingTypeCustom && data.biddingTypeCustom.trim().length > 0 } return true }, { message: "기타 입찰유형을 선택한 경우 직접 입력해주세요", path: ["biddingTypeCustom"] }) export const updateBiddingSchema = z.object({ biddingNumber: z.string().min(1, "입찰번호는 필수입니다").optional(), revision: z.number().int().min(0).optional(), projectId: z.number().min(1).optional(), projectName: z.string().optional(), itemName: z.string().min(1, "품목명은 필수입니다").optional(), title: z.string().min(1, "입찰명은 필수입니다").optional(), description: z.string().optional(), content: z.string().optional(), // 입찰공고 정보 noticeType: z.enum(['standard', 'facility', 'unit_price']).optional(), contractType: z.enum(biddings.contractType.enumValues).optional(), biddingType: z.enum(biddings.biddingType.enumValues).optional(), biddingTypeCustom: z.string().optional(), awardCount: z.enum(biddings.awardCount.enumValues).optional(), // 가격 정보 (조회용) budget: z.string().optional(), finalBidPrice: z.string().optional(), targetPrice: z.string().optional(), // PR 정보 (조회용) prNumber: z.string().optional(), // 계약기간 contractStartDate: z.string().optional(), contractEndDate: z.string().optional(), submissionStartDate: z.string().optional(), submissionEndDate: z.string().optional(), hasSpecificationMeeting: z.boolean().optional(), hasPrDocument: z.boolean().optional(), currency: z.string().optional(), status: z.enum(biddings.status.enumValues).optional(), isPublic: z.boolean().optional(), isUrgent: z.boolean().optional(), // 구매조직 purchasingOrganization: z.string().optional(), // 담당자 정보 (개선된 구조) bidPicId: z.number().int().positive().optional(), bidPicName: z.string().min(1, "입찰담당자는 필수입니다").optional(), bidPicCode: z.string().min(1, "입찰담당자 코드는 필수입니다").optional(), supplyPicId: z.number().int().positive().optional(), supplyPicName: z.string().optional(), supplyPicCode: z.string().optional(), // 기존 담당자 정보 (점진적 마이그레이션을 위해 유지) managerName: z.string().optional(), managerEmail: z.string().email().optional().or(z.literal("")), managerPhone: z.string().optional(), // 구매요청자 (현재 사용자) requesterName: z.string().optional(), // 입찰 조건 biddingConditions: z.object({ paymentTerms: z.string().min(1, "SHI 지급조건은 필수입니다").optional(), taxConditions: z.string().min(1, "SHI 매입부가가치세는 필수입니다").optional(), incoterms: z.string().min(1, "SHI 인도조건은 필수입니다").optional(), incotermsOption: z.string().optional(), contractDeliveryDate: z.string().optional(), shippingPort: z.string().optional(), destinationPort: z.string().optional(), isPriceAdjustmentApplicable: z.boolean().default(false), sparePartOptions: z.string().optional(), }), remarks: z.string().optional(), }) export type GetBiddingsSchema = Awaited> export type CreateBiddingSchema = z.infer export type UpdateBiddingSchema = z.infer // === 상세 페이지용 검증 스키마들 === // 내정가 업데이트 스키마 export const updateTargetPriceSchema = z.object({ biddingId: z.number().int().positive('입찰 ID는 필수입니다'), targetPrice: z.number().min(0, '내정가는 0 이상이어야 합니다'), }) // 협력업체 정보 생성 스키마 export const createQuotationVendorSchema = z.object({ biddingId: z.number().int().positive('입찰 ID는 필수입니다'), vendorId: z.number().int().positive('업체 ID는 필수입니다'), vendorName: z.string().min(1, '업체명은 필수입니다'), vendorCode: z.string().min(1, '업체코드는 필수입니다'), contactPerson: z.string().optional(), contactEmail: z.string().email().optional().or(z.literal('')), contactPhone: z.string().optional(), quotationAmount: z.number().min(0, '견적금액은 0 이상이어야 합니다'), currency: z.string().min(1, '통화는 필수입니다').default('KRW'), paymentTerms: z.string().optional(), taxConditions: z.string().optional(), deliveryDate: z.string().optional(), awardRatio: z.number().min(0).max(100, '발주비율은 0-100 사이여야 합니다').optional(), invitationStatus: z.enum(['pending', 'pre_quote_sent', 'pre_quote_accepted', 'pre_quote_declined', 'pre_quote_submitted', 'bidding_sent', 'bidding_accepted', 'bidding_declined', 'bidding_cancelled', 'bidding_submitted']).default('pending'), }) // 협력업체 정보 업데이트 스키마 export const updateQuotationVendorSchema = z.object({ id: z.number().int().positive('협력업체 ID는 필수입니다'), vendorName: z.string().min(1, '업체명은 필수입니다').optional(), vendorCode: z.string().min(1, '업체코드는 필수입니다').optional(), contactPerson: z.string().optional(), contactEmail: z.string().email().optional().or(z.literal('')), contactPhone: z.string().optional(), quotationAmount: z.number().min(0, '견적금액은 0 이상이어야 합니다').optional(), currency: z.string().min(1, '통화는 필수입니다').optional(), paymentTerms: z.string().optional(), taxConditions: z.string().optional(), deliveryDate: z.string().optional(), awardRatio: z.number().min(0).max(100, '발주비율은 0-100 사이여야 합니다').optional(), invitationStatus: z.enum(['pending', 'pre_quote_sent', 'pre_quote_accepted', 'pre_quote_declined', 'pre_quote_submitted', 'bidding_sent', 'bidding_accepted', 'bidding_declined', 'bidding_cancelled', 'bidding_submitted']).optional(), }) // 낙찰 선택 스키마 export const selectWinnerSchema = z.object({ biddingId: z.number().int().positive('입찰 ID는 필수입니다'), vendorId: z.number().int().positive('업체 ID는 필수입니다'), awardRatio: z.number().min(0).max(100, '발주비율은 0-100 사이여야 합니다'), }) // 입찰 상태 변경 스키마 export const updateBiddingStatusSchema = z.object({ biddingId: z.number().int().positive('입찰 ID는 필수입니다'), status: z.enum(biddings.status.enumValues, { required_error: '입찰 상태는 필수입니다' }), }) export type UpdateTargetPriceSchema = z.infer export type CreateQuotationVendorSchema = z.infer export type UpdateQuotationVendorSchema = z.infer export type SelectWinnerSchema = z.infer export type UpdateBiddingStatusSchema = z.infer