feat(WEB): CEO 대시보드 캘린더 강화 및 validation 모듈 분리

- CalendarSection 일정 CRUD 기능 확장 (상세 모달 연동)
- ScheduleDetailModal 개선
- CEO 대시보드 섹션별 API 키 통일
- validation.ts → validation/ 모듈 분리 (item-schemas, utils)
- formatters.ts 확장
- date.ts 유틸 추가
- SignupPage/EmployeeForm/AddCompanyDialog 등 소규모 개선
- PaymentHistory/PopupManagement utils 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-24 13:01:41 +09:00
parent 6a469181cd
commit b8dfa3d887
24 changed files with 726 additions and 502 deletions

View File

@@ -165,6 +165,14 @@ export function parseNumber(formatted: string): number {
return isNaN(num) ? 0 : num;
}
/**
* 숫자만 추출 (범용)
* 전화번호, 사업자번호 등 포맷팅 전처리에 사용
*/
export function extractDigits(value: string): string {
return value.replace(/\D/g, '');
}
/**
* Leading zero 제거 (01 → 1)
*/
@@ -188,34 +196,3 @@ export function removeLeadingZeros(value: string): string {
return value.replace(/^0+/, '') || '0';
}
/**
* 숫자만 추출 (음수, 소수점 허용 옵션)
*/
export function extractNumbers(value: string, options?: {
allowNegative?: boolean;
allowDecimal?: boolean;
}): string {
const { allowNegative = false, allowDecimal = false } = options || {};
let pattern = '\\d';
if (allowNegative) pattern = '-?' + pattern;
if (allowDecimal) pattern = pattern + '|\\.';
const regex = new RegExp(`[^${allowNegative ? '-' : ''}${allowDecimal ? '.' : ''}\\d]`, 'g');
let result = value.replace(regex, '');
// 중복 마이너스 제거 (첫 번째만 유지)
if (allowNegative && result.includes('-')) {
const isNegative = result.startsWith('-');
result = result.replace(/-/g, '');
if (isNegative) result = '-' + result;
}
// 중복 소수점 제거 (첫 번째만 유지)
if (allowDecimal && result.includes('.')) {
const parts = result.split('.');
result = parts[0] + (parts.length > 1 ? '.' + parts.slice(1).join('') : '');
}
return result;
}

View File

@@ -58,6 +58,17 @@ export function formatDateForInput(dateStr: string | null | undefined): string {
return getLocalDateString(date);
}
/**
* ISO 문자열에서 날짜 부분(YYYY-MM-DD)만 추출
* null/undefined 시 빈 문자열 반환 (폼 데이터 변환용)
* @example toDateString("2025-01-06T00:00:00.000Z") // "2025-01-06"
* @example toDateString(null) // ""
*/
export function toDateString(isoString: string | null | undefined): string {
if (!isoString) return '';
return isoString.split('T')[0];
}
/**
* 날짜 표시용 포맷 (YYYY-MM-DD)
* @example formatDate("2025-01-06T00:00:00.000Z") // "2025-01-06"

View File

@@ -0,0 +1,110 @@
/**
* 공통 Zod 검증 스키마
*
* 품목명, 품목유형, 날짜, 숫자, BOM 등 여러 스키마에서 공유하는 기본 블록
*/
import { z } from 'zod';
// ===== 내부 전용 스키마 =====
/**
* 품목 코드 검증
* 형식: {업체코드}-{품목유형}-{일련번호}
* 예: KD-FG-001
*
* 현재 사용하지 않음 (품목 코드 자동 생성)
*/
export const _itemCodeSchema = z.string()
.min(1, '품목 코드를 입력해주세요')
.regex(
/^[A-Z0-9]+-[A-Z]{2}-\d+$/,
'품목 코드 형식이 올바르지 않습니다 (예: KD-FG-001)'
);
// ===== 공통 필드 스키마 =====
/**
* 품목명 검증
*/
export const itemNameSchema = z.preprocess(
(val) => val === undefined || val === null ? "" : val,
z.string().min(1, '품목명을 입력해주세요').max(200, '품목명은 200자 이내로 입력해주세요')
);
/**
* 품목 유형 검증
*/
export const itemTypeSchema = z.enum(['FG', 'PT', 'SM', 'RM', 'CS'], {
message: '품목 유형을 선택해주세요',
});
/**
* 단위 검증
*
* 현재 사용하지 않음 (materialUnitSchema로 대체)
*/
export const _unitSchema = z.string()
.min(1, '단위를 입력해주세요')
.max(20, '단위는 20자 이내로 입력해주세요');
/**
* 양수 검증 (가격, 수량 등)
* undefined나 빈 문자열은 검증하지 않음
*/
export 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는 검증하지 않음
*/
export 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(),
});

View File

@@ -0,0 +1,191 @@
/**
* 폼 데이터 Zod 검증 스키마
*
* 품목 생성/수정/필터용 스키마
*/
import { z } from 'zod';
import { itemTypeSchema, bendingDetailSchema } from './common';
import {
productSchema,
productSchemaBase,
partSchemaBase,
materialSchemaBase,
materialSchema,
consumableSchemaBase,
consumableSchema,
} from './item-schemas';
// ===== 폼 데이터 스키마 (생성/수정용) =====
/**
* 품목 생성 폼 스키마
* (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(),
});

View File

@@ -0,0 +1,31 @@
/**
* Zod 검증 스키마 barrel export
*
* 기존 `@/lib/utils/validation` 경로 호환성 유지
*/
// common
export { bendingDetailSchema, bomLineSchema } from './common';
// item-schemas
export {
productSchema,
partSchema,
materialSchema,
consumableSchema,
itemMasterSchema,
} from './item-schemas';
// form-schemas
export { createItemFormSchema, updateItemFormSchema, itemFilterSchema } from './form-schemas';
// utils
export { getSchemaByItemType, formatZodError } from './utils';
export type {
ItemMasterFormData,
CreateItemFormData,
UpdateItemFormData,
ItemFilterFormData,
BOMLineFormData,
BendingDetailFormData,
} from './utils';

View File

@@ -1,119 +1,25 @@
/**
* Zod
* Zod
*
* react-hook-form과
* FG(), PT(), SM/RM(/), CS()
*/
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(),
});
import {
itemNameSchema,
itemTypeSchema,
dateSchema,
positiveNumberSchema,
bomLineSchema,
bendingDetailSchema,
} from './common';
// ===== 품목 마스터 기본 스키마 =====
/**
*
*/
const itemMasterBaseSchema = z.object({
export const itemMasterBaseSchema = z.object({
// 공통 필수 필드
itemCode: z.string().optional(), // 자동생성되므로 선택 사항
itemName: itemNameSchema,
@@ -185,7 +91,7 @@ const productFieldsSchema = z.object({
* (FG) (refinement )
*
*/
const productSchemaBase = itemMasterBaseSchema
export const productSchemaBase = itemMasterBaseSchema
.omit({
purchasePrice: true,
salesPrice: true,
@@ -268,7 +174,7 @@ const partFieldsSchema = z.object({
* (PT) (refinement )
* itemName을
*/
const partSchemaBase = itemMasterBaseSchema
export const partSchemaBase = itemMasterBaseSchema
.extend({
itemName: z.string().max(200).optional(), // 부품은 itemName 선택 사항
})
@@ -422,7 +328,7 @@ const materialUnitSchema = z.preprocess(
* / Base (refinement , )
* specification, unit을 (z.object로 )
*/
const materialSchemaBase = z.object({
export const materialSchemaBase = z.object({
// 공통 필수 필드
itemCode: z.string().optional(),
itemName: itemNameSchema,
@@ -479,7 +385,7 @@ export const materialSchema = materialSchemaBase;
* Base
* specification, unit을
*/
const consumableSchemaBase = itemMasterBaseSchema
export const consumableSchemaBase = itemMasterBaseSchema
.extend({
specification: materialSpecificationSchema, // optional → 필수로 변경
unit: materialUnitSchema, // optional → 필수로 변경
@@ -505,221 +411,3 @@ export const itemMasterSchema = z.discriminatedUnion('itemType', [
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;
}

View File

@@ -0,0 +1,64 @@
/**
* 검증 유틸리티 함수 및 타입 추출
*/
import { z } from 'zod';
import type { ItemType } from '@/types/item';
import { bendingDetailSchema, bomLineSchema } from './common';
import {
productSchema,
partSchema,
materialSchema,
consumableSchema,
itemMasterSchema,
itemMasterBaseSchema,
} from './item-schemas';
import {
createItemFormSchema,
updateItemFormSchema,
itemFilterSchema,
} from './form-schemas';
// ===== 타입 추출 =====
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;
}