본문 바로가기
TIL

TIL 240708 - class-validator, custom validator

by lemonpie611 2024. 7. 8.

Nest.js에서 body로 들어오는 요청의 유효성 검사를 위해서는 class-validator, class-transformer가 구축된 validation pipe를 사용한다.

아래 코드처럼 입력들에 대해 각각 dto를 만들고 validation pipe를 전역에 깔아놔서 모든 입력 유효성 검사를 할 수 있도록 했다.

import { PickType } from '@nestjs/mapped-types';
import { CreateTicketDto } from './createTicket.dto';
import { IsNotEmpty, IsString } from 'class-validator';

export class UpdateTicketDto extends PickType(CreateTicketDto, ['receiverAddress']) {
    @IsString()
    @IsNotEmpty({message: '비밀번호를 입력해주세요.'})
    password: string;
}

 

그리고 여느때와 다름없이 dto를 열심히 짜던 중 개큰 문제를 마주함,,

import { IsBoolean, IsDateString, IsEmail, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from "class-validator";

export class CreateTicketDto {
    @IsBoolean()
    @IsOptional()
    useUserInfo: boolean;

    @IsDateString()
    @IsNotEmpty({message: '본인확인을 위해 생년월일을 입력해주세요.'})
    receiverBirthDate: string;

    @IsPhoneNumber('KR')
    @IsOptional()
    receiverPhoneNumber: string;

    @IsString()
    @IsOptional()
    receiverAddress: string;
}

 

내가 원하는건 useUserInfo가 false이거나 값이 들어오지 않으면 receiverPhoneNumber, receiverAddress를 필수로 받고, true이면 받지 않아도 되도록 만드는 것이었다.. class-validator에 그런 데코레이터를 제공할 리가 없어서 custom validator를 직접 만들어야했다.

 

걍 저 기능을 없앨까 했는데

일단 해보기로함^^..

 

기본 양식은 이렇다.

export class CustomTextValidator implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    // Custom validation logic
    const isValid = /* 여기서 validation 로직 작성 */;
    return isValid;
  }

  defaultMessage(args: ValidationArguments) {
    // Custom error message
    return '에러 메시지!!';
  }
}

 

여기서 value랑 args가 뭐냐면

export class GeometryClass {
  @IsString()
  @IsNotEmpty()
  type: string;

  @IsNumber()
  @IsNotEmpty()
  @Validate(CustomValidator, ['type'])
  nums: number;
}

 

데코레이터가 사용되는 자리에서 nums가 value로 들어가고, ['type'], 즉 추가로 입력받은 나머지 값들이 args로 들어간다.

 

내가 짠 custom validator는 이렇게 생겼다.

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, Validate } from 'class-validator';

@ValidatorConstraint({ name: 'useUserInfoOption', async: false })
export class UseUserInfoConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const [useUserInfoProperty, optionalProperty1, optionalProperty2] = args.constraints;
    const useUserInfo = (args.object as any)[useUserInfoProperty];
    const optionalValue1 = (args.object as any)[optionalProperty1];
    const optionalValue2 = (args.object as any)[optionalProperty2];
    return this.isValid(useUserInfo, optionalValue1, optionalValue2);
  }

  private isValid (useUserInfo: boolean, value1: string, value2: string): boolean {
    if (useUserInfo==true) {
        return true;
    }
    if (value1!=undefined && value2!=undefined) {
        return true;
    }
    return false;
  }

  defaultMessage(args: ValidationArguments) {
    return '수령자 정보를 입력해주세요.';
  }
}

 

앞에서 말한것처럼 useUserInfo의 true/false 값에 따라 나머지 두 값의 입력 여부를 판단하는 validator이다.

 

dto에 적용

import { IsBoolean, IsDateString, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, Validate } from "class-validator";
import { UseUserInfoConstraint } from "src/utils/useUserInfo.decorator";

export class CreateTicketDto {
    @IsOptional()
    @IsBoolean()
    useUserInfo: boolean;

    @IsDateString()
    @IsNotEmpty({message: '본인확인을 위해 생년월일을 입력해주세요.'})
    receiverBirthDate: string;

    @IsOptional()
    @IsPhoneNumber('KR')
    @Validate(UseUserInfoConstraint, ['useUserInfo', 'receiverPhoneNumber', 'receiverAddress'])
    receiverPhoneNumber: string;

    @IsOptional()
    @IsString()
    @Validate(UseUserInfoConstraint, ['useUserInfo', 'receiverPhoneNumber', 'receiverAddress'])
    receiverAddress: string;
}

 

그리고 이걸 다른 곳에서도 사용할 수 있도록 코드를 유연하게 수정하면 아래와 같아진다.

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

@ValidatorConstraint({ name: 'useUserInfoOption', async: false })
export class UseUserInfoConstraint implements ValidatorConstraintInterface {
  validate(boolValue: boolean, args: ValidationArguments) {
    let arr = [];
    for (let property of args.constraints) {
      const value = (args.object as any)[property];
      arr.push(value);
    }
    return this.isValid(boolValue, arr);
  }

  private isValid(boolValue: boolean, arr: any[]): boolean {
    if (boolValue == true) {
      return true;
    } else {
      for (let cur of arr) {
        if (cur==undefined) {
          return false;
        }
      }
      return true;
    }
  }

  defaultMessage(args: ValidationArguments) {
    return '수령자 정보를 입력해주세요.';
  }
}