import { foxValidationMessage, foxValidators, type FoxValidationMessages, type FoxValidator, } from '@fox/core/validation'; import { ADMIN_ROLE_OPTIONS, type AdminRoleCode, } from '@/lib/domain/admin-member'; export const ADMIN_LOGIN_ID_HELP_TEXT = '영어 소문자, 숫자를 조합하여 입력 후 중복여부를 확인하세요.'; export const ADMIN_PASSWORD_HELP_TEXT = '영어, 숫자, 특수문자 중 2종류 이상 조합, 최소 10자리 이상'; export const ADMIN_PASSWORD_ERROR_TEXT = '비밀번호가 규칙에 맞지 않습니다. 영어·숫자·특수문자 중 2종류 이상으로 10자리 이상 입력해 주세요.'; const LOGIN_ID_MIN_LENGTH = 4; const LOGIN_ID_MAX_LENGTH = 20; export const ADMIN_LOGIN_ID_VALIDATORS: FoxValidator[] = [ foxValidators.required, foxValidators.minLength(LOGIN_ID_MIN_LENGTH), foxValidators.maxLength(LOGIN_ID_MAX_LENGTH), foxValidators.pattern(/^[a-z0-9]+$/), foxValidators.characterKinds(2), ]; export const ADMIN_LOGIN_ID_MESSAGES: FoxValidationMessages = { required: 'ID를 입력해 주세요.', minlength: `ID는 ${LOGIN_ID_MIN_LENGTH}~${LOGIN_ID_MAX_LENGTH}자로 입력해 주세요.`, maxlength: `ID는 ${LOGIN_ID_MIN_LENGTH}~${LOGIN_ID_MAX_LENGTH}자로 입력해 주세요.`, pattern: ADMIN_LOGIN_ID_HELP_TEXT, characterKinds: ADMIN_LOGIN_ID_HELP_TEXT, }; export const ADMIN_PASSWORD_POLICY = { minLength: 10, kinds: 2, } as const; const NAME_MAX_LENGTH = 50; const EMAIL_MAX_LENGTH = 100; export function adminEmailValidators(required: boolean): FoxValidator[] { return [ ...(required ? [foxValidators.required] : []), foxValidators.email, foxValidators.maxLength(EMAIL_MAX_LENGTH), ]; } export const ADMIN_EMAIL_MESSAGES: FoxValidationMessages = { required: '이메일을 입력해 주세요.', email: '이메일 형식이 올바르지 않습니다.', maxlength: `이메일은 ${EMAIL_MAX_LENGTH}자 이내로 입력해 주세요.`, }; const PHONE_DIGITS_PATTERN = /^(\d{3})(\d{3,4})(\d{4})$/; export type AdminMemberEditableValues = { password: string; phoneNumber: string; email: string; roleCode: AdminRoleCode; }; export type AdminMemberCreateValues = AdminMemberEditableValues & { name: string; loginId: string; }; export type AdminMemberFormErrors = Partial< Record >; export type ValidationResult = | { ok: true; values: T } | { ok: false; errors: AdminMemberFormErrors }; export type AdminMemberFormState = | { status: 'idle' } | { status: 'error'; message?: string; errors?: AdminMemberFormErrors } | { status: 'success' }; export const INITIAL_ADMIN_MEMBER_FORM_STATE: AdminMemberFormState = { status: 'idle', }; function countCharacterKinds(password: string): number { const kinds = [/[a-zA-Z]/, /\d/, /[^a-zA-Z0-9]/]; return kinds.filter((pattern) => pattern.test(password)).length; } export function validateAdminLoginId(loginId: string): string | null { const errors = foxValidators.compose(ADMIN_LOGIN_ID_VALIDATORS)(loginId.trim()); return foxValidationMessage(errors, ADMIN_LOGIN_ID_MESSAGES) ?? null; } export function formatPhoneNumber(digits: string): string | null { const matched = PHONE_DIGITS_PATTERN.exec(digits.trim()); return matched === null ? null : `${matched[1]}-${matched[2]}-${matched[3]}`; } export function toPhoneDigits(phoneNumber: string | null): string { return (phoneNumber ?? '').replace(/\D/g, ''); } function validateEditableValues( values: AdminMemberEditableValues, errors: AdminMemberFormErrors, options: { passwordRequired: boolean; emailRequired: boolean } ): AdminMemberEditableValues { const password = values.password; const phoneNumber = values.phoneNumber.trim(); const email = values.email.trim(); if (!password) { if (options.passwordRequired) { errors.password = ADMIN_PASSWORD_ERROR_TEXT; } } else if ( password.length < ADMIN_PASSWORD_POLICY.minLength || countCharacterKinds(password) < ADMIN_PASSWORD_POLICY.kinds ) { errors.password = ADMIN_PASSWORD_ERROR_TEXT; } if (!phoneNumber) { errors.phoneNumber = '휴대전화 번호를 정확히 입력해 주세요.'; } const emailError = foxValidationMessage( foxValidators.compose(adminEmailValidators(options.emailRequired))(email), ADMIN_EMAIL_MESSAGES ); if (emailError) { errors.email = emailError; } if (!ADMIN_ROLE_OPTIONS.some((option) => option.value === values.roleCode)) { errors.roleCode = '역할을 선택해 주세요.'; } return { password, phoneNumber, email, roleCode: values.roleCode }; } export function validateAdminMemberCreate( values: AdminMemberCreateValues ): ValidationResult { const errors: AdminMemberFormErrors = {}; const name = values.name.trim(); if (!name) { errors.name = '이름을 입력해 주세요.'; } else if (name.length > NAME_MAX_LENGTH) { errors.name = `이름은 ${NAME_MAX_LENGTH}자 이내로 입력해 주세요.`; } const loginIdError = validateAdminLoginId(values.loginId); if (loginIdError) { errors.loginId = loginIdError; } const editable = validateEditableValues(values, errors, { passwordRequired: true, emailRequired: true, }); if (Object.keys(errors).length > 0) { return { ok: false, errors }; } return { ok: true, values: { ...editable, name, loginId: values.loginId.trim() }, }; } export function validateAdminMemberUpdate( values: AdminMemberEditableValues ): ValidationResult { const errors: AdminMemberFormErrors = {}; const editable = validateEditableValues(values, errors, { passwordRequired: false, emailRequired: false, }); if (Object.keys(errors).length > 0) { return { ok: false, errors }; } return { ok: true, values: editable }; }