export const POINT_MAX = 1_000_000; export const DESCRIPTION_MAX_LENGTH = 200; export interface PointStandardValues { /** 수정은 기존 값, 등록은 비어 있다(백엔드가 채번). */ id: string; /** 공통코드 `CODE_GROUP.pointEvent`. 등록은 선택, 수정은 읽기 전용으로 그대로 돌려보낸다. */ eventCode: string; point: number; limitPoint: number; isDuplicable: boolean; description: string; isActive: boolean; } export type PointStandardFormErrors = Partial< Record<'id' | 'eventCode' | 'point' | 'limitPoint' | 'description', string> >; export type PointStandardFormState = | { status: 'idle' } | { status: 'error'; message?: string; errors?: PointStandardFormErrors } | { status: 'success' }; export const INITIAL_POINT_STANDARD_FORM_STATE: PointStandardFormState = { status: 'idle', }; type ValidationResult = | { ok: true; values: PointStandardValues } | { ok: false; errors: PointStandardFormErrors }; function validatePointNumber( value: number, label: string ): { error?: string } { if (!Number.isInteger(value)) { return { error: `${label}는 숫자만 입력할 수 있습니다.` }; } if (value < 0) { return { error: `${label}는 0 이상으로 입력해 주세요.` }; } if (value > POINT_MAX) { return { error: `${label}는 ${POINT_MAX.toLocaleString('ko-KR')} 이하로 입력해 주세요.`, }; } return {}; } /** * 등록·수정 공통 검증. * * `isCreate`로 갈린다 — 등록은 대상 id가 없고(백엔드가 채번) 이벤트를 골라야 하며, 수정은 그 * 반대다(이벤트는 읽기 전용이라 화면이 그대로 돌려보낸다). */ export function validatePointStandard( values: PointStandardValues, options: { isCreate: boolean; allowedEventCodes?: readonly string[] } ): ValidationResult { const errors: PointStandardFormErrors = {}; if (!options.isCreate && !values.id.trim()) { errors.id = '수정할 대상을 찾을 수 없습니다.'; } const eventCode = values.eventCode.trim(); if (!eventCode) { errors.eventCode = '이벤트를 선택해 주세요.'; } else if ( options.isCreate && options.allowedEventCodes && options.allowedEventCodes.length > 0 && !options.allowedEventCodes.includes(eventCode) ) { errors.eventCode = '이벤트를 다시 선택해 주세요.'; } const point = validatePointNumber(values.point, '포인트'); if (point.error) { errors.point = point.error; } const limitPoint = validatePointNumber(values.limitPoint, '제한포인트'); if (limitPoint.error) { errors.limitPoint = limitPoint.error; } const description = values.description.trim(); if (!description) { errors.description = '이벤트 설명을 입력해 주세요.'; } else if (description.length > DESCRIPTION_MAX_LENGTH) { errors.description = `이벤트 설명은 ${DESCRIPTION_MAX_LENGTH}자 이내로 입력해 주세요.`; } if (Object.keys(errors).length > 0) { return { ok: false, errors }; } return { ok: true, values: { id: values.id.trim(), eventCode, point: values.point, limitPoint: values.limitPoint, isDuplicable: values.isDuplicable, description, isActive: values.isActive, }, }; }