Spring

[Spring] 입력값 검증 (Validator)

아잠만_ 2024. 8. 9. 14:36

Hibernate-Validator

bean(자바 빈 클래스, VO)의 유효성 검사의 기능을 이용해 서버측에서 입력값 검증을 하는 라이브러리

빈 클래스 앞에 

@Validated

를 붙이면 활성화 된다

Bean Validation이 제공하는 제약 애너테이션

  • NotNull : 빈 값 체크(int타입)
  • NotBlank : null 체크, trim후 길이가 0인지 체크(String타입)
  • Size : 글자 수 체크
  • Email : 이메일 주소 형식 체크
  • Past : 오늘보다 과거 날짜(ex. 생일)
  • Future : 미래 날짜 체크(ex. 예약일)
  • DateTimeFormat(pattern="yyyy-MM-dd") : 해당하는 패턴의 날짜 String을 Date로 변환
  • AssertFalse : false 값만 통과 가능
  • AssertTrue : true 값만 통과 가능
  • DecimalMax(value=) : 지정된 값 이하의 실수만 통과 가능
  • DecimalMin(value=) : 지정된 값 이상의 실수만 통과 가능
  • Digits(integer=,fraction=) : 대상 수가 지정된 정수와 소수 자리수보다 적을 경우 통과 가능
  • Future : 대상 날짜가 현재보다 미래일 경우만 통과 가능
  • Past : 대상 날짜가 현재보다 과거일 경우만 통과 가능
  • Max(value) : 지정된 값보다 아래일 경우만 통과 가능
  • Min(value) : 지정된 값보다 이상일 경우만 통과 가능
  • NotNull : null 값이 아닐 경우만 통과 가능
  • Null : null일 겨우만 통과 가능
  • Pattern(regex=, flag=) : 해당 정규식을 만족할 경우만 통과 가능
  • Size(min=, max=) : 문자열 또는 배열이 지정된 값 사이일 경우 통과 가능
  • Valid : 대상 객체의 확인 조건을 만족할 경우 통과 가능
@NotBlank
@Size(max = 100)
private String prodName;

초기 설정

pom.xml

<!-- 입력값을 검증하기 위한 라이브러리 의존 관계 정의 시작 
      스프링 
      M(Model) : Service, ServiceImple, Mapper
      V(View) : JSP
      C(Controller) : Controller 
      Bean(자바빈 클래스, ArticleVO) Validation(유효성검사) 기능을 이용해 
      요청 파라미터 값이 바인딩된(멤버변수에 세팅된) 도메인 클래스(ArticleVO)의 입력값 검증을 함
      요청 파라미터 : ?articleNo=112&title=개똥이
      public String write(골뱅이ModelAttribute ArticleVO articleVO)
      -->
      <!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
      <dependency>
          <groupId>org.hibernate</groupId>
          <artifactId>hibernate-validator</artifactId>
          <version>5.2.5.Final</version>
      </dependency>

예제

ProdController.java (부분)

	@PostMapping("/registPost")
    // 입력값 검증 기능을 활성화
	public String registPost(@Validated ProdVO prodVo) {
		log.info("vo >>"+prodVo);
		int result = this.service.registPost(prodVo);
		log.info("result >> "+result);
//		return "redirect: /prod/list";
		return "redirect: /prod/detail?prodId="+prodVo.getProdId();
	}
    
    // error처리를 위해 form에
    // ModelAttribute를 넣었으므로 해당 메서드도 prodVO 추가
    @GetMapping("/regist")
	public String regist(Model model,@ModelAttribute ProdVO prodVO) {
		return "prod/regist";
	}
    
    // forward를 위해 비동기화로 변경
    @ResponseBody
	@GetMapping("/lprod")
	public List<LprodVO> lprodList(){
		List<LprodVO> lprod = this.service.lprodList();
		return lprod;
	}

ProdVO.java

package kr.or.ddit.vo;

import java.util.Date;
import java.util.List;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

import org.hibernate.validator.constraints.NotBlank;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.multipart.MultipartFile;

import lombok.Data;

@Data
public class ProdVO {        
	private int rnum;
	private String prodId;
	// message는 내가 지정하고 싶은 오류 메세지
	// 기본값 : 반드시 값이 존재하고 공백 문자를 제외한 길이가 0보다 커야 합니다.
	@NotBlank(message = "상품명은 필수 입력 항목입니다.")
	private String prodName;
	private String prodLgu;
	private String prodBuyer;
	private int prodCost;
	private int prodPrice;
	@NotNull(message = "판매가는 필수 입력 항목입니다.")
	private int prodSale;
	private String prodOu;
	private String prodDetail;
	private String prodImg;
	private int prodTotalstock;
	//String 타입인 "2024-08-09" -> Date타입으로 변환
	@DateTimeFormat(pattern="yyyy-MM-dd")
	private Date prodInsdate; // 최근 입고일자
	private String prodOutline;
	private int prodProperstock;
	private String prodSize;
	private String prodColor;
	private String prodDelivery;
	private String prodUnit;
	private int prodQtyin;
	private int prodQtysale;
	private int prodMileage;
	private long fileGroupNo;
	private String prodDelYn; 
	
	private MultipartFile[] uploadFile;
	
	private List<CartVO> cartVoList;
	private FileGroupVO fgvo;
}

regist.jsp

redirect로 보낼 수 없기때문에 lprod 비동기화로 변경

error 코드 추가, form태그로 변경 (error띄울 input은 form:input으로 변환 id, name > path)

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<link type="text/css" href="/resources/ckeditor5/sample/css/sample.css" rel="stylesheet" media="screen"/>
<script type="text/javascript" src="/resources/ckeditor5/ckeditor.js"></script>
<script type="text/javascript" src="/resources/js/jquery.min.js"></script>
<div class="card card-success">
	<div class="card-header">
		<h2>상품 등록</h2>
	</div>

	<form:form modelAttribute="prodVO"
	 id="frm" action="/prod/registPost" method="post" enctype="multipart/form-data">
		<div class="card-body">
			<div class="row">
				<div class="col-sm-6">
					<!-- text input -->
					<div class="form-group">
						<label for="prodId">상품 코드</label> <input required type="text"
							class="form-control" readonly placeholder="상품분류를 선택해주세요"
							name="prodId" id="prodId">
					</div>
				</div>
				<div class="col-sm-6">
					<div class="form-group">
						<label for="prodLgu">상품 분류 코드</label> <select required="required" class="form-control"
							id="prodLgu" name="prodLgu">
							<option value="" selected="selected" disabled="disabled">선택해주세요</option>
							
						</select>
					</div>
				</div>
			</div>
			<div class="row">
				<div class="col-sm-6">
					<!-- text input -->
					<div class="form-group">
						<label for="prodName">상품 명</label> 
						<form:input class="form-control" placeholder="상품명" path="prodName" />
						<code style="color:red;">
							<form:errors path="prodName"></form:errors>
						</code>
					</div>
				</div>
				<div class="col-sm-6">
					<div class="form-group">
						<label for="prodBuyer">거래처</label> <select required class="form-control"
							id="prodBuyer" name="prodBuyer">
							<option value="" selected="selected" disabled="disabled">상품 분류를 선택해주세요</option>
						</select>
					</div>
				</div>
			</div>
			<div class="row">
				<div class="col-sm-6">
					<!-- textarea -->
					<div class="form-group">
						<label for="prodSale">상품 판매가</label> 
						<input type="number" class="form-control" placeholder="판매가" name="prodSale"/>
						<code style="color:red;">
							${errors.prodSale}
						</code>
					</div>
				</div>
				<div class="col-sm-6">
					<div class="form-group">
						<label>업로드 파일</label>
						<div class="custom-file">
							<input required type="file" class="custom-file-input" name="uploadFile"
								id="uploadFile" multiple="multiple"> <label class="custom-file-label" id="fileLabel"
								for="uploadFile">파일을 선택해주세요</label>
						</div>
					</div>
					<div id="pImg">
<!-- 						<img src="/resources/images/P1234.jpg" style="width: 100px; border:1px solid #ced4da;border-radius: .25rem;" /> -->
					</div>
				</div>
			</div>
			<div class="row">
				<div class="col-sm-6">
					<!-- textarea -->
					<div class="form-group">
						<label for="prodSale">최근 입고일자</label> 
						<form:input placeholder="ex)2024-08-09" class="form-control" path="prodInsdate"/>
						<code style="color:red;">
							<form:errors path="prodInsdate"></form:errors>
						</code>
					</div>
				</div>
			</div>
			<!-- input states -->
			<div class="form-group">
				<label class="col-form-label" for="prodDetail">상품 상세 설명</label>
				<div id="prodDetailTemp"></div>
				<textarea hidden class="form-control" rows="3" id="prodDetail"
					placeholder="상품 상세 설명" name="prodDetail" cols=""></textarea>
			</div>
		</div>
		<!-- /.card-body -->
		<div class="card-footer row" style="justify-content: space-between;">
			<div>
				<a href="/prod/list" class="btn btn-info">목록</a>
			</div>
			<div>
				<button type="submit" class="btn btn-warning">등록</button>
			</div>
			<div>
				<button type="reset" id="reset" class="btn btn-secondary">초기화</button>
			</div>
		</div>
	</form:form>
</div>
<script>
// uploadUrl => 이미지 업로드 시 요청할 요청URI
// editor => CKEditor가 생성된 후 바로 그 객체
// window.editor : 그 객체를 이렇게 부르겠다 정의
ClassicEditor.create( document.querySelector('#prodDetailTemp'),{ckfinder:{uploadUrl:'/image/upload'}})
 .then(editor=>{window.editor=editor;})
 .catch(err=>{console.error(err.stack);});



document.getElementById('uploadFile').addEventListener('change', function(event) {
    const files = event.target.files; // 업로드된 파일 목록
    const imagePreviewContainer = document.getElementById('pImg');
    imagePreviewContainer.innerHTML = ''; // 기존 미리보기 초기화

    // 업로드된 파일을 미리보기로 표시
    for (let i = 0; i < files.length; i++) {
        const file = files[i];
        const reader = new FileReader();

        reader.onload = function(e) {
            const imgContainer = document.createElement('div');
            imgContainer.className = 'image-container';
            imgContainer.style.position = 'relative';
            imgContainer.style.display = 'inline-block';
            imgContainer.style.margin = '5px';

            // 이미지 생성
            const img = document.createElement('img');
            img.src = e.target.result; // FileReader로 읽은 이미지 URL
            img.style.width = '100px';
            img.style.border = '1px solid #ced4da';
            img.style.borderRadius = '.25rem';

            // 삭제 버튼 생성
            const button = document.createElement('button');
            button.type = 'button';
            button.className = 'btn btn-tool';
            button.style.position = 'absolute';
            button.style.top = '5px';
            button.style.right = '-1px';
            button.style.background = 'rgba(255, 255, 255, 0.8)';
            button.style.border = 'none';
            button.style.borderRadius = '50%';
            button.style.padding = '2px';
            button.style.cursor = 'pointer';
            button.innerHTML = '<i class="fas fa-times"></i>';

            // 삭제 버튼 클릭 시 동작
            button.onclick = function() {
                // 이미지 컨테이너 삭제
                imgContainer.remove();

                // 업로드된 파일 목록에서 해당 파일 삭제
                const fileList = Array.from(document.getElementById('uploadFile').files);
                const newFileList = fileList.filter((_, index) => index !== i); // 현재 인덱스 제외
                const dataTransfer = new DataTransfer(); // 새로운 파일 리스트 생성

                newFileList.forEach(file => dataTransfer.items.add(file)); // 새로운 파일 리스트 추가
                document.getElementById('uploadFile').files = dataTransfer.files; // 파일 입력 필드 업데이트
            };

            // 이미지 컨테이너에 이미지와 버튼 추가
            imgContainer.appendChild(img);
            imgContainer.appendChild(button);
            imagePreviewContainer.appendChild(imgContainer);
        };

        reader.readAsDataURL(file); // 파일을 데이터 URL로 읽기
    }
});

function removeImage(button) {
    // Find the parent container of the button
    const imageContainer = button.closest('.image-container');

    // Remove the image container from the DOM
    if (imageContainer) {
        imageContainer.remove();
    }
}

$(function(){
	$.ajax({
		url : "/prod/lprod",
		success : function(res){
			let str = ""
			console.log(res);
			$.each(res, function(){
				str+= "<option value='"+this.lprodGu+"'>"+this.lprodNm+"</option>";
			})
			$('#prodLgu').append(str);
		}
	})
	
	
	$(".ck-blurred").keydown(function(){
		console.log("str : " + window.editor.getData());

		$("#prodDetail").val(window.editor.getData());
		});

		$(".ck-blurred").on("focusout",function(){
		$("#prodDetail").val(window.editor.getData());
	});
	
	$('#prodLgu').on('change',function(){
		let lgu = $('#prodLgu').val();
		// prodId 생성
		$.ajax({
			url : "/prod/createProdId",
			data : JSON.stringify({"prodLgu" : lgu}),
			contentType : 'application/json',
			type : "post",
			success : function(res){
				$('#prodId').val(res);
			}
		})
		
		// buyer select 설정
		$.ajax({
			url : "/prod/buyer",
			data : JSON.stringify({"buyerLgu" : lgu}),
			contentType : 'application/json',
			type : "post",
			success : function(res){
				console.log(res);
				let str="<option value='' selected disabled>선택해주세요</option>";
				$.each(res, function(){
					str += "<option value='"+this.buyerId+"'>"+this.buyerName+"</option>";
				})
				if(res.length==0){
					str = "<option value='' selected disabled>해당하는 거래처가 없습니다</option>"
				}
				$('#prodBuyer').html(str);
			}
		})
	})
	
	// reset시 buyer select도 리셋
	$('#reset').on('click',function(){
		let str="<option value='' selected disabled>상품 분류를 선택해주세요</option>";
		$('#prodBuyer').html(str);
	})
})
</script>