Files
sam-react-prod/src/lib/utils/validation/item-schemas.ts

414 lines
12 KiB
TypeScript
Raw Normal View History

/**
* 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') }),
]);