- BOMItem Omit 타입 시그니처 통일 (useTemplateManagement, SectionsTab, ItemMasterContext) - HeadersInit → Record<string, string> 타입 변경 - Zustand useShallow 마이그레이션 (zustand/react/shallow) - DataTable, ListPageTemplate 제네릭 타입 제약 추가 - 설정 관리 페이지 추가 (직급, 직책, 휴가정책, 근무일정, 권한) - HR 관리 페이지 추가 (급여, 휴가) - 단가관리 페이지 리팩토링 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
725 lines
22 KiB
TypeScript
725 lines
22 KiB
TypeScript
/**
|
|
* Zod 검증 스키마
|
|
*
|
|
* react-hook-form과 함께 사용하는 폼 검증
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import type { ItemType } from '@/types/item';
|
|
|
|
// ===== 공통 스키마 =====
|
|
|
|
/**
|
|
* 품목 코드 검증
|
|
* 형식: {업체코드}-{품목유형}-{일련번호}
|
|
* 예: KD-FG-001
|
|
*
|
|
* 현재 사용하지 않음 (품목 코드 자동 생성)
|
|
*/
|
|
const _itemCodeSchema = z.string()
|
|
.min(1, '품목 코드를 입력해주세요')
|
|
.regex(
|
|
/^[A-Z0-9]+-[A-Z]{2}-\d+$/,
|
|
'품목 코드 형식이 올바르지 않습니다 (예: KD-FG-001)'
|
|
);
|
|
|
|
/**
|
|
* 품목명 검증
|
|
*/
|
|
const itemNameSchema = z.preprocess(
|
|
(val) => val === undefined || val === null ? "" : val,
|
|
z.string().min(1, '품목명을 입력해주세요').max(200, '품목명은 200자 이내로 입력해주세요')
|
|
);
|
|
|
|
/**
|
|
* 품목 유형 검증
|
|
*/
|
|
const itemTypeSchema = z.enum(['FG', 'PT', 'SM', 'RM', 'CS'], {
|
|
message: '품목 유형을 선택해주세요',
|
|
});
|
|
|
|
/**
|
|
* 단위 검증
|
|
*
|
|
* 현재 사용하지 않음 (materialUnitSchema로 대체)
|
|
*/
|
|
const _unitSchema = z.string()
|
|
.min(1, '단위를 입력해주세요')
|
|
.max(20, '단위는 20자 이내로 입력해주세요');
|
|
|
|
/**
|
|
* 양수 검증 (가격, 수량 등)
|
|
* undefined나 빈 문자열은 검증하지 않음
|
|
*/
|
|
const positiveNumberSchema = z.union([
|
|
z.number().positive('0보다 큰 값을 입력해주세요'),
|
|
z.string().transform((val) => parseFloat(val)).pipe(z.number().positive('0보다 큰 값을 입력해주세요')),
|
|
z.undefined(),
|
|
z.null(),
|
|
z.literal('')
|
|
]).optional();
|
|
|
|
/**
|
|
* 날짜 검증 (YYYY-MM-DD)
|
|
* 빈 문자열이나 undefined는 검증하지 않음
|
|
*/
|
|
const dateSchema = z.preprocess(
|
|
(val) => {
|
|
if (val === undefined || val === null || val === '') return undefined;
|
|
return val;
|
|
},
|
|
z.string()
|
|
.regex(/^\d{4}-\d{2}-\d{2}$/, '날짜 형식이 올바르지 않습니다 (YYYY-MM-DD)')
|
|
.optional()
|
|
);
|
|
|
|
// ===== BOM 라인 스키마 =====
|
|
|
|
/**
|
|
* 절곡품 전개도 상세 스키마
|
|
*/
|
|
export const bendingDetailSchema = z.object({
|
|
id: z.string(),
|
|
no: z.number().int().positive(),
|
|
input: z.number(),
|
|
elongation: z.number().default(-1),
|
|
calculated: z.number(),
|
|
sum: z.number(),
|
|
shaded: z.boolean().default(false),
|
|
aAngle: z.number().optional(),
|
|
});
|
|
|
|
/**
|
|
* BOM 라인 스키마
|
|
*/
|
|
export const bomLineSchema = z.object({
|
|
id: z.string(),
|
|
childItemCode: z.string().min(1, '하위 품목 코드를 입력해주세요'),
|
|
childItemName: z.string().min(1, '하위 품목명을 입력해주세요'),
|
|
quantity: z.number().positive('수량은 0보다 커야 합니다'),
|
|
unit: z.string().min(1, '단위를 입력해주세요'),
|
|
unitPrice: positiveNumberSchema,
|
|
quantityFormula: z.string().optional(),
|
|
note: z.string().max(500).optional(),
|
|
|
|
// 절곡품 관련
|
|
isBending: z.boolean().optional(),
|
|
bendingDiagram: z.string().url().optional(),
|
|
bendingDetails: z.array(bendingDetailSchema).optional(),
|
|
});
|
|
|
|
// ===== 품목 마스터 기본 스키마 =====
|
|
|
|
/**
|
|
* 품목 마스터 공통 필드
|
|
*/
|
|
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 없이)
|
|
* 제품에는 가격 정보가 없으므로 제거
|
|
*/
|
|
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을 사용하지 않으므로 선택 사항으로 변경
|
|
*/
|
|
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로 완전히 새로 정의)
|
|
*/
|
|
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을 필수로 오버라이드
|
|
*/
|
|
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') }),
|
|
]);
|
|
|
|
// ===== 폼 데이터 스키마 (생성/수정용) =====
|
|
|
|
/**
|
|
* 품목 생성 폼 스키마
|
|
* (id, createdAt, updatedAt 제외)
|
|
*
|
|
* discriminatedUnion은 omit()을 지원하지 않으므로,
|
|
* 각 스키마에 대해 개별적으로 omit을 적용합니다.
|
|
*/
|
|
// partSchemaBase를 omit한 후 itemType merge - refinement는 마지막에 적용
|
|
const partSchemaForForm = partSchemaBase
|
|
.omit({ createdAt: true, updatedAt: true })
|
|
.merge(z.object({ itemType: z.literal('PT') }))
|
|
.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'],
|
|
});
|
|
return; // 품목명이 없으면 더 이상 검증하지 않음 (설치유형 등 체크 안 함)
|
|
}
|
|
|
|
// 3단계: 조립 부품 전용 필드 (partType과 category1이 모두 있을 때만 실행됨)
|
|
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'],
|
|
});
|
|
}
|
|
}
|
|
|
|
// 절곡품 전용 필드 (partType과 category1이 모두 있을 때만 실행됨)
|
|
if (data.partType === 'BENDING') {
|
|
// 단계별 검증: 종류(category2) → 재질(material) → 폭 합계 → 모양&길이
|
|
if (!data.category2 || data.category2 === '') {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: '종류를 선택해주세요',
|
|
path: ['category2'],
|
|
});
|
|
return; // 종류가 없으면 재질, 폭 합계, 모양&길이 체크 안 함
|
|
}
|
|
if (!data.material) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: '재질을 선택해주세요',
|
|
path: ['material'],
|
|
});
|
|
return; // 재질이 없으면 폭 합계, 모양&길이 체크 안 함
|
|
}
|
|
if (!data.length) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: '폭 합계를 입력해주세요',
|
|
path: ['length'],
|
|
});
|
|
return; // 폭 합계가 없으면 모양&길이 체크 안 함
|
|
}
|
|
if (!data.bendingLength) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: '모양&길이를 선택해주세요',
|
|
path: ['bendingLength'],
|
|
});
|
|
}
|
|
}
|
|
|
|
// 구매 부품 전용 필드 (partType과 category1이 모두 있을 때만 실행됨)
|
|
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'],
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
export const createItemFormSchema = z.discriminatedUnion('itemType', [
|
|
productSchema.omit({ createdAt: true, updatedAt: true }).extend({ itemType: z.literal('FG') }),
|
|
partSchemaForForm, // itemType이 이미 merge되어 있음
|
|
materialSchemaBase.omit({ createdAt: true, updatedAt: true }).extend({ itemType: z.literal('SM') }),
|
|
materialSchemaBase.omit({ createdAt: true, updatedAt: true }).extend({ itemType: z.literal('RM') }),
|
|
consumableSchemaBase.omit({ createdAt: true, updatedAt: true }).extend({ itemType: z.literal('CS') }),
|
|
]);
|
|
|
|
/**
|
|
* 품목 수정 폼 스키마
|
|
* (모든 필드 선택적)
|
|
*
|
|
* discriminatedUnion은 partial()도 지원하지 않으므로,
|
|
* 각 스키마에 대해 개별적으로 처리합니다.
|
|
*/
|
|
export const updateItemFormSchema = z.discriminatedUnion('itemType', [
|
|
productSchemaBase.omit({ createdAt: true, updatedAt: true }).partial().extend({ itemType: z.literal('FG') }),
|
|
partSchemaBase.omit({ createdAt: true, updatedAt: true }).partial().extend({ itemType: z.literal('PT') }),
|
|
materialSchemaBase.omit({ createdAt: true, updatedAt: true }).partial().extend({ itemType: z.literal('SM') }),
|
|
materialSchemaBase.omit({ createdAt: true, updatedAt: true }).partial().extend({ itemType: z.literal('RM') }),
|
|
consumableSchemaBase.omit({ createdAt: true, updatedAt: true }).partial().extend({ itemType: z.literal('CS') }),
|
|
]);
|
|
|
|
// ===== 필터 스키마 =====
|
|
|
|
/**
|
|
* 품목 목록 필터 스키마
|
|
*/
|
|
export const itemFilterSchema = z.object({
|
|
itemType: itemTypeSchema.optional(),
|
|
search: z.string().optional(),
|
|
category1: z.string().optional(),
|
|
category2: z.string().optional(),
|
|
category3: z.string().optional(),
|
|
isActive: z.boolean().optional(),
|
|
});
|
|
|
|
// ===== 타입 추출 =====
|
|
|
|
export type ItemMasterFormData = z.infer<typeof itemMasterSchema>;
|
|
export type CreateItemFormData = z.infer<typeof createItemFormSchema>;
|
|
export type UpdateItemFormData = z.infer<typeof updateItemFormSchema>;
|
|
export type ItemFilterFormData = z.infer<typeof itemFilterSchema>;
|
|
export type BOMLineFormData = z.infer<typeof bomLineSchema>;
|
|
export type BendingDetailFormData = z.infer<typeof bendingDetailSchema>;
|
|
|
|
// ===== 유틸리티 함수 =====
|
|
|
|
/**
|
|
* 품목 유형에 따른 스키마 선택
|
|
*/
|
|
export function getSchemaByItemType(itemType: ItemType) {
|
|
switch (itemType) {
|
|
case 'FG':
|
|
return productSchema;
|
|
case 'PT':
|
|
return partSchema;
|
|
case 'SM':
|
|
case 'RM':
|
|
return materialSchema;
|
|
case 'CS':
|
|
return consumableSchema;
|
|
default:
|
|
return itemMasterBaseSchema;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 에러 메시지 한글화
|
|
*/
|
|
export function formatZodError(error: z.ZodError): Record<string, string> {
|
|
const formatted: Record<string, string> = {};
|
|
|
|
error.issues.forEach((err) => {
|
|
const path = err.path.join('.');
|
|
formatted[path] = err.message;
|
|
});
|
|
|
|
return formatted;
|
|
} |