/** * 품목유형별 Zod 검증 스키마 * * FG(제품), PT(부품), SM/RM(원자재/부자재), CS(소모품) 스키마 정의 */ import { z } from 'zod'; import { itemNameSchema, itemTypeSchema, dateSchema, positiveNumberSchema, bomLineSchema, bendingDetailSchema, } from './common'; // ===== 품목 마스터 기본 스키마 ===== /** * 품목 마스터 공통 필드 */ export const itemMasterBaseSchema = z.object({ // 공통 필수 필드 itemCode: z.string().optional(), // 자동생성되므로 선택 사항 itemName: itemNameSchema, itemType: itemTypeSchema, unit: z.string().max(20).optional(), // 선택 사항으로 변경 // 공통 선택 필드 specification: z.string().max(500).optional(), isActive: z.boolean().default(true), // 분류 category1: z.string().max(100).optional(), category2: z.string().max(100).optional(), category3: z.string().max(100).optional(), // 가격 정보 - 모두 선택 사항 purchasePrice: z.number().optional(), salesPrice: z.number().optional(), marginRate: z.number().min(0).max(100).optional(), processingCost: z.number().optional(), laborCost: z.number().optional(), installCost: z.number().optional(), // BOM bom: z.array(bomLineSchema).optional(), bomCategories: z.array(z.string()).optional(), // 메타데이터 safetyStock: z.number().int().nonnegative().optional(), leadTime: z.number().int().nonnegative().optional(), isVariableSize: z.boolean().optional(), // 버전 관리 currentRevision: z.number().int().nonnegative().default(0), isFinal: z.boolean().default(false), finalizedDate: dateSchema, finalizedBy: z.string().optional(), // 시스템 필드 createdAt: z.string().optional(), updatedAt: z.string().optional(), }); // ===== 제품(FG) 스키마 ===== /** * 제품 전용 필드 */ const productFieldsSchema = z.object({ productName: z.preprocess( (val) => val === undefined || val === null ? "" : val, z.string().min(1, '상품명을 입력해주세요').max(200, '상품명은 200자 이내로 입력해주세요') ), productCategory: z.enum(['SCREEN', 'STEEL']).optional(), lotAbbreviation: z.string().max(10).optional(), note: z.string().max(1000).optional(), // 인정 정보 certificationNumber: z.string().max(100).optional(), certificationStartDate: dateSchema, certificationEndDate: dateSchema, specificationFile: z.string().optional(), specificationFileName: z.string().optional(), certificationFile: z.string().optional(), certificationFileName: z.string().optional(), }); /** * 제품(FG) 전체 스키마 (refinement 없이) * 제품에는 가격 정보가 없으므로 제거 */ export const productSchemaBase = itemMasterBaseSchema .omit({ purchasePrice: true, salesPrice: true, processingCost: true, laborCost: true, installCost: true, }) .merge(productFieldsSchema); /** * 제품(FG) 전체 스키마 (refinement 포함) */ export const productSchema = productSchemaBase.refine( (data) => { // 인정 정보 검증: 시작일과 종료일이 모두 있거나 모두 없어야 함 if (data.certificationStartDate && data.certificationEndDate) { return new Date(data.certificationStartDate) <= new Date(data.certificationEndDate); } return true; }, { message: '인정 유효기간 종료일은 시작일보다 이후여야 합니다', path: ['certificationEndDate'], } ); // ===== 부품(PT) 스키마 ===== /** * 부품 전용 필드 */ const partFieldsSchema = z.object({ partType: z.preprocess( (val) => val === undefined || val === null ? "" : val, z.string() .min(1, '부품 유형을 선택해주세요') .refine( (val) => ['ASSEMBLY', 'BENDING', 'PURCHASED'].includes(val), { message: '부품 유형을 선택해주세요' } ) ), partUsage: z.enum(['GUIDE_RAIL', 'BOTTOM_FINISH', 'CASE', 'DOOR', 'BRACKET', 'GENERAL']).optional(), // 조립 부품 installationType: z.string().max(50).optional(), assemblyType: z.string().max(50).optional(), sideSpecWidth: z.string().max(50).optional(), sideSpecHeight: z.string().max(50).optional(), assemblyLength: z.string().max(50).optional(), // 가이드레일 guideRailModelType: z.string().max(100).optional(), guideRailModel: z.string().max(100).optional(), // 절곡품 bendingDiagram: z.string().url().optional(), bendingDetails: z.array(bendingDetailSchema).optional(), material: z.string().max(100).optional(), length: z.string().max(50).optional(), bendingLength: z.string().max(50).optional(), // 구매 부품 electricOpenerPower: z.string().max(50).optional(), electricOpenerCapacity: z.string().max(50).optional(), motorVoltage: z.string().max(50).optional(), motorCapacity: z.string().max(50).optional(), chainSpec: z.string().max(100).optional(), // 인정 정보 (부품도 인정 가능) certificationNumber: z.string().max(100).optional(), certificationStartDate: dateSchema, certificationEndDate: dateSchema, specificationFile: z.string().optional(), specificationFileName: z.string().optional(), certificationFile: z.string().optional(), certificationFileName: z.string().optional(), }); /** * 부품(PT) 전체 스키마 (refinement 없이) * 부품은 itemName을 사용하지 않으므로 선택 사항으로 변경 */ export const partSchemaBase = itemMasterBaseSchema .extend({ itemName: z.string().max(200).optional(), // 부품은 itemName 선택 사항 }) .merge(partFieldsSchema); /** * 부품(PT) 전체 스키마 (refinement 포함) */ export const partSchema = partSchemaBase .superRefine((data, ctx) => { // 1단계: 부품 유형 필수 if (!data.partType || data.partType === '') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '부품 유형을 선택해주세요', path: ['partType'], }); return; // 부품 유형이 없으면 더 이상 검증하지 않음 } // 2단계: 부품 유형이 있을 때만 품목명 필수 if (!data.category1 || data.category1 === '') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '품목명을 선택해주세요', path: ['category1'], }); } // 3단계: 조립 부품 전용 필드 if (data.partType === 'ASSEMBLY') { if (!data.installationType) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '설치 유형을 선택해주세요', path: ['installationType'], }); } if (!data.sideSpecWidth) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '측면 규격 (가로)를 입력해주세요', path: ['sideSpecWidth'], }); } if (!data.sideSpecHeight) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '측면 규격 (세로)를 입력해주세요', path: ['sideSpecHeight'], }); } if (!data.assemblyLength) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '길이를 선택해주세요', path: ['assemblyLength'], }); } } // 절곡품 전용 필드 if (data.partType === 'BENDING') { if (!data.material) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '재질을 선택해주세요', path: ['material'], }); } if (!data.length) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '폭 합계를 입력해주세요', path: ['length'], }); } if (!data.bendingLength) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '모양&길이를 선택해주세요', path: ['bendingLength'], }); } } // 구매 부품 전용 필드 if (data.partType === 'PURCHASED') { if (data.category1 === 'electric_opener') { if (!data.electricOpenerPower) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '전원을 선택해주세요', path: ['electricOpenerPower'], }); } if (!data.electricOpenerCapacity) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '용량을 선택해주세요', path: ['electricOpenerCapacity'], }); } } if (data.category1 === 'motor' && !data.motorVoltage) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '전압을 선택해주세요', path: ['motorVoltage'], }); } if (data.category1 === 'chain' && !data.chainSpec) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: '체인 규격을 선택해주세요', path: ['chainSpec'], }); } } }); // ===== 원자재/부자재(RM/SM) 스키마 ===== /** * 원자재/부자재 전용 필드 * * 현재 사용하지 않음 (materialSchemaBase에 직접 포함됨) */ const _materialFieldsSchema = z.object({ material: z.string().max(100).optional(), length: z.string().max(50).optional(), }); /** * 원자재/부자재 규격 필수 필드 스키마 */ const materialSpecificationSchema = z.preprocess( (val) => val === undefined || val === null ? "" : val, z.string().min(1, '규격을 입력해주세요').max(500, '최대 500자') ); /** * 원자재/부자재 단위 필수 필드 스키마 */ const materialUnitSchema = z.preprocess( (val) => val === undefined || val === null ? "" : val, z.string().min(1, '단위를 입력해주세요').max(20, '최대 20자') ); /** * 원자재/부자재 Base 스키마 (refinement 없음, 필드만 정의) * specification, unit을 필수로 정의 (z.object로 완전히 새로 정의) */ export const materialSchemaBase = z.object({ // 공통 필수 필드 itemCode: z.string().optional(), itemName: itemNameSchema, itemType: itemTypeSchema, specification: materialSpecificationSchema, // 필수! unit: materialUnitSchema, // 필수! isActive: z.boolean().default(true), // 분류 category1: z.string().max(100).optional(), category2: z.string().max(100).optional(), category3: z.string().max(100).optional(), // 가격 정보 purchasePrice: z.number().optional(), salesPrice: z.number().optional(), marginRate: z.number().min(0).max(100).optional(), processingCost: z.number().optional(), laborCost: z.number().optional(), installCost: z.number().optional(), // BOM bom: z.array(bomLineSchema).optional(), bomCategories: z.array(z.string()).optional(), // 메타데이터 safetyStock: z.number().int().nonnegative().optional(), leadTime: z.number().int().nonnegative().optional(), isVariableSize: z.boolean().optional(), // 버전 관리 currentRevision: z.number().int().nonnegative().default(0), isFinal: z.boolean().default(false), finalizedDate: dateSchema, finalizedBy: z.string().optional(), // 시스템 필드 createdAt: z.string().optional(), updatedAt: z.string().optional(), // 원자재/부자재 전용 필드 material: z.string().max(100).optional(), length: z.string().max(50).optional(), }); /** * 원자재/부자재 전체 스키마 (export용) */ export const materialSchema = materialSchemaBase; // ===== 소모품(CS) 스키마 ===== /** * 소모품 Base 스키마 * specification, unit을 필수로 오버라이드 */ export const consumableSchemaBase = itemMasterBaseSchema .extend({ specification: materialSpecificationSchema, // optional → 필수로 변경 unit: materialUnitSchema, // optional → 필수로 변경 }); /** * 소모품 전체 스키마 (export용) */ export const consumableSchema = consumableSchemaBase; // ===== 통합 품목 스키마 ===== /** * 품목 유형에 따른 동적 검증 * * Zod 4.x에서는 refinement가 있는 스키마를 extend할 수 없으므로, * refinement가 없는 base 스키마를 사용합니다. */ export const itemMasterSchema = z.discriminatedUnion('itemType', [ productSchemaBase.extend({ itemType: z.literal('FG') }), partSchemaBase.extend({ itemType: z.literal('PT') }), materialSchema.extend({ itemType: z.literal('SM') }), materialSchema.extend({ itemType: z.literal('RM') }), consumableSchema.extend({ itemType: z.literal('CS') }), ]);