/** * 오류 객체 → 화면 문구. * * 검증기와 분리한 이유는 Angular가 `Validators`에 문구를 두지 않은 이유와 같다 — 같은 규칙도 * 자리에 따라 다른 말로 안내해야 한다("10자 이상"과 "비밀번호는 10자 이상"). 검증기는 무엇이 * 틀렸는지만 말하고, 그것을 무슨 말로 옮길지는 여기서(또는 호출부의 `overrides`로) 정한다. * * 한 번에 **한 문구만** 낸다 — 입력 칸 아래 헬퍼 자리가 한 줄이고, 오류를 쌓아 보여주면 어느 * 것부터 고쳐야 할지 알기 어렵다. 순서는 `MESSAGE_ORDER`가 정한다(비어 있음 → 길이 → 조합). */ import type { FoxValidationErrors } from './fox-validators'; /** 오류 키별 기본 문구. 값이 필요한 것은 오류 객체의 맥락을 받아 만든다. */ type MessageFactory = (detail: unknown) => string; function detailOf(detail: unknown): T { return detail as T; } const DEFAULT_MESSAGES: Record = { required: () => '필수 입력 항목입니다.', minlength: (detail) => `${detailOf<{ requiredLength: number }>(detail).requiredLength}자 이상 입력해 주세요.`, maxlength: (detail) => `${detailOf<{ requiredLength: number }>(detail).requiredLength}자 이내로 입력해 주세요.`, pattern: () => '형식이 올바르지 않습니다.', email: () => '이메일 형식이 올바르지 않습니다.', characterKinds: (detail) => `영문, 숫자, 특수문자 중 ${ detailOf<{ requiredKinds: number }>(detail).requiredKinds }종류 이상을 섞어 주세요.`, repeatedCharacters: (detail) => `같은 문자를 ${detailOf<{ max: number }>(detail).max}번까지만 연달아 쓸 수 있습니다.`, sequentialCharacters: (detail) => `연속된 문자나 숫자를 ${ detailOf<{ max: number }>(detail).max }자를 넘겨 쓸 수 없습니다.`, notContaining: (detail) => `${detailOf<{ forbidden: string }>(detail).forbidden}은(는) 포함할 수 없습니다.`, equalTo: () => '값이 일치하지 않습니다.', }; /** 먼저 고쳐야 하는 것부터. 여기 없는 키는 뒤에 남은 순서대로 본다. */ const MESSAGE_ORDER = [ 'required', 'minlength', 'maxlength', 'pattern', 'email', 'characterKinds', 'repeatedCharacters', 'sequentialCharacters', 'notContaining', 'equalTo', ]; /** 오류 키 → 문구 덮어쓰기. 문자열이면 그대로, 함수면 맥락을 받아 만든다. */ export type FoxValidationMessages = Record< string, string | ((detail: unknown) => string) >; /** * 오류 객체에서 보여줄 문구 하나를 고른다. 오류가 없으면 `undefined`. * 덮어쓰기에 없는 키는 기본 문구로 떨어지고, 기본 문구에도 없으면 키를 그대로 쓰지 않고 * `undefined`를 돌려준다 — 정체불명의 영문 키를 사용자에게 보이지 않기 위해서다. */ export function foxValidationMessage( errors: FoxValidationErrors | null, overrides: FoxValidationMessages = {} ): string | undefined { if (!errors) { return undefined; } const keys = Object.keys(errors); const ordered = [ ...MESSAGE_ORDER.filter((key) => keys.includes(key)), ...keys.filter((key) => !MESSAGE_ORDER.includes(key)), ]; for (const key of ordered) { const override = overrides[key]; if (typeof override === 'string') { return override; } if (typeof override === 'function') { return override(errors[key]); } const fallback = DEFAULT_MESSAGES[key]; if (fallback) { return fallback(errors[key]); } } return undefined; }