/** * 코드관리 등록/수정 입력 규칙 — 순수 검증 로직만 담는다(외부 의존 없음). * * 시안 네 팝업(공통코드 등록/수정, 상세코드 등록/수정)이 이 파일의 규칙을 공유한다. * **이 파일이 검증의 단일 진실원천이다** — Server Action이 저장 직전에 여기를 거친다. * * 시안의 필수(*) 표시는 상세코드 팝업에만 있지만(코드·코드값의미·정렬번호), 공통코드도 * 코드ID·코드명 없이는 저장할 수 없으므로 같은 강도로 검증한다 — 화면 표시와 무관하게 * 백엔드가 PK로 쓰는 값이다. * * `CommonCodeFormState`가 이 파일에 있는 이유는 Next.js가 `'use server'` 파일에서 함수가 아닌 * 값을 export하는 것을 런타임에 거부하기 때문이다(꾸미기 아이템 폼과 같은 사정). */ const CODE_MAX_LENGTH = 50; const NAME_MAX_LENGTH = 100; const DESCRIPTION_MAX_LENGTH = 500; /** 코드값은 영문 대문자·숫자·언더스코어만 받는다 — 코드테이블 키라 공백·한글이 섞이면 곤란하다. */ const CODE_PATTERN = /^[A-Z0-9_]+$/; /** 공통코드(그룹) 등록·수정이 실제로 바꿀 수 있는 항목. */ export type CommonCodeGroupValues = { comCd: string; name: string; description: string; }; /** 상세코드 등록·수정이 실제로 바꿀 수 있는 항목. */ export type CommonCodeDetailValues = { /** 소속 그룹. 화면에서는 읽기 전용이지만 저장 대상이라 값으로 다룬다. */ comCd: string; comDtlCd: string; /** 시안의 "코드값의미" — 백엔드 `cdNm`이다. */ name: string; description: string; sortSeq: number; }; export type CommonCodeFormErrors = Partial< Record< keyof CommonCodeGroupValues | keyof CommonCodeDetailValues, string > >; export type ValidationResult = | { ok: true; values: T } | { ok: false; errors: CommonCodeFormErrors }; export type CommonCodeFormState = | { status: 'idle' } | { status: 'error'; message?: string; errors?: CommonCodeFormErrors } | { status: 'success' }; export const INITIAL_COMMON_CODE_FORM_STATE: CommonCodeFormState = { status: 'idle', }; /** 코드값 공통 검증 — 그룹의 `comCd`와 상세의 `comDtlCd`가 같은 규칙을 쓴다. */ function validateCode( raw: string, label: string ): { value: string; error?: string } { // 코드는 대문자로 정규화한다 — 소문자로 저장되면 조회 조건과 어긋난다. const value = raw.trim().toUpperCase(); if (!value) { return { value, error: `${label}를 입력해 주세요.` }; } if (value.length > CODE_MAX_LENGTH) { return { value, error: `${label}는 ${CODE_MAX_LENGTH}자 이내로 입력해 주세요.`, }; } if (!CODE_PATTERN.test(value)) { return { value, error: `${label}는 영문 대문자·숫자·_만 사용할 수 있습니다.` }; } return { value }; } function validateName(raw: string, label: string): { value: string; error?: string } { const value = raw.trim(); if (!value) { return { value, error: `${label}을 입력해 주세요.` }; } if (value.length > NAME_MAX_LENGTH) { return { value, error: `${label}은 ${NAME_MAX_LENGTH}자 이내로 입력해 주세요.`, }; } return { value }; } function validateDescription(raw: string): { value: string; error?: string } { const value = raw.trim(); if (value.length > DESCRIPTION_MAX_LENGTH) { return { value, error: `설명은 ${DESCRIPTION_MAX_LENGTH}자 이내로 입력해 주세요.`, }; } return { value }; } /** 시안 공통코드 등록/수정 — 수정은 코드ID가 읽기 전용이라 값이 폼이 아니라 경로에서 온다. */ export function validateCommonCodeGroup( values: CommonCodeGroupValues ): ValidationResult { const errors: CommonCodeFormErrors = {}; const code = validateCode(values.comCd, '코드ID'); if (code.error) { errors.comCd = code.error; } const name = validateName(values.name, '코드ID명'); if (name.error) { errors.name = name.error; } const description = validateDescription(values.description); if (description.error) { errors.description = description.error; } if (Object.keys(errors).length > 0) { return { ok: false, errors }; } return { ok: true, values: { comCd: code.value, name: name.value, description: description.value, }, }; } /** 시안 상세코드 등록/수정. */ export function validateCommonCodeDetail( values: CommonCodeDetailValues ): ValidationResult { const errors: CommonCodeFormErrors = {}; const group = validateCode(values.comCd, '코드ID'); if (group.error) { // 상세코드 팝업의 코드ID는 좌측 선택에서 오는 읽기 전용 값이라, 여기가 비었다는 것은 // 공통코드를 고르지 않고 저장이 시도됐다는 뜻이다. errors.comCd = '공통코드를 먼저 선택해 주세요.'; } const code = validateCode(values.comDtlCd, '코드'); if (code.error) { errors.comDtlCd = code.error; } const name = validateName(values.name, '코드값의미'); if (name.error) { errors.name = name.error; } const description = validateDescription(values.description); if (description.error) { errors.description = description.error; } if (!Number.isInteger(values.sortSeq) || values.sortSeq < 1) { errors.sortSeq = '정렬번호는 1 이상의 숫자로 입력해 주세요.'; } if (Object.keys(errors).length > 0) { return { ok: false, errors }; } return { ok: true, values: { comCd: group.value, comDtlCd: code.value, name: name.value, description: description.value, sortSeq: values.sortSeq, }, }; }