728x90

게시글 작성 및 수정 작업 중 @Valid 를 사용하지 않고 자바스크립트를 통해 form 데이터를 검증하도록 하였다.

관련 프로젝트 : https://black-mint.tistory.com/70

 

[Spring Boot] 게시글 작성, 수정

이전 파일 업로드 글과 연관지어 게시글 작성의 구현을 마무리하고 이를 정리하겠습니다. 파일첨부에 대해서는 언급하지 않겠습니다! 이전 글 : https://black-mint.tistory.com/62?category=982467 [Spring Boot].

black-mint.tistory.com


1. html 코드

			<form action="#" th:action="@{/board/form}" th:object="${board}" method="post" enctype="multipart/form-data"
				onsubmit="return checkForm(this);">
				<div class="mb-3">
					<label for="title" class="form-label">제목</label>
					<input type="text" class="form-control" id="title" th:field="*{title}">
				</div>
				<div class="mb-3">
					<label for="content" class="form-label">내용</label>
					<textarea class="form-control" id="content" rows="13" th:field="*{content}"></textarea>
				</div>

				<div class="nav justify-content-end mb-5">
					<button id="submit" type="submit" class="me-2 btn btn-primary">작성</button>
				</div>
			</form>
  • onsubmit을 이용해 form 양식 제출 시 값을 검증한다.
  • checkForm(this) : this를 넣어줌으로써 자바스크립트에서 form 태그에 담긴 데이터를 컨트롤하는데 용이

2. 검증 script 코드

		// form submit 검증
		function checkForm(form) {
			if (form.title.value == "") {
				alert('제목을 입력하세요.');
				form.title.classList.add('is-invalid');
				form.title.focus();
				return false;
			}

			if (form.content.value == "") {
				alert('내용을 입력하세요.');
				form.title.classList.remove('is-invalid');
				form.content.classList.add('is-invalid');
				form.content.focus();
				return false;
			}
			form.content.classList.remove('is-invalid');
			return true;
		}
  • 파라미터로 넘어온 form(this)를 이용해 값을 확인한다.
  • form.[name].value : 제출된 [name] 이름을 가진 element의 값
  • classList.add : 해당 태그에 클래스를 추가한다.
  • classList.remove : 해당 태그의 클래스를 제거한다.
  • focus() : 해당 태그로 커서를 옮긴다.
  • return false의 경우 서버로 값이 넘어가지 않음.
728x90

이전 파일 업로드 글과 연관지어 게시글 작성의 구현을 마무리하고 이를 정리하겠습니다.

파일첨부에 대해서는 언급하지 않겠습니다!

이전 글 : https://black-mint.tistory.com/62?category=982467 

 

[Spring Boot] 파일 첨부 (게시글 이미지 첨부)

게시글 작성에서 파일 첨부하는 방법을 기록한다. (본 글은 이미지 첨부 방법을 다룸) 1. Mysql - file 테이블 CREATE TABLE `tb_file` ( `id` int NOT NULL AUTO_INCREMENT, `board_id` int NOT NULL, `original_..

black-mint.tistory.com

 


 

1. Mysql - 테이블

CREATE TABLE `tb_board` (
  `id` int NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `content` text NOT NULL,
  `writer_id` int NOT NULL,
  `writer` varchar(20) NOT NULL,
  `delete_yn` varchar(1) DEFAULT 'N',
  `create_date` datetime DEFAULT NULL,
  `views` int DEFAULT '0',
  `type` varchar(20) DEFAULT 'board',
  PRIMARY KEY (`id`),
  KEY `writer_id` (`writer_id`),
  CONSTRAINT `tb_board_ibfk_1` FOREIGN KEY (`writer_id`) REFERENCES `tb_userinfo` (`id`) ON DELETE CASCADE
)

 

2. BoardDTO

@Data
public class Board {
    private Long id;
    private String title;
    private String content;
    private Long writerId;
    private String writer;
    private String deleteYN;
    private Timestamp createDate;
    private Long views;
    private String type;
}

 

3. 게시글 작성 Controller

// 게시글 신규 작성 폼 진입 & 기존 게시글 불러오기
@GetMapping("/form")
public String form(Model model, @RequestParam(required = false) Long boardId) {
    if (boardId == null) {
        model.addAttribute("board", new Board());
    } else {
        Board board = boardService.contentLoad(boardId, "board");
        model.addAttribute("board", board);
    }

    return "board/form";
}
  • 게시글 작성, 수정 화면으로 이동하는 GetMapping
  • 기존 게시글을 수정하는 경우 기존 게시글 내용을 추가해주기 위해 Service를 통해 정보를 얻어와 addAttribute를 진행

 

// 게시글 작성 & 수정
@PostMapping("/form")
public String boardSubmit(Board board, Principal principal,
                          @RequestParam(value = "files", required = false) List<MultipartFile> files,
                          @RequestParam(value = "boardId", required = false) Long boardId) throws IOException, SQLException {
    if (board.getTitle().length() < 1 || board.getContent().length() < 1 || (!CollectionUtils.isEmpty(files) && files.size() > 7)) {
        // 잘못된 입력값이 들어왔을 때 다시 해당 페이지로 로딩
        if(boardId != null) {
            return "redirect:/board/form?boardId=" + boardId;
        }
        return "redirect:/board/form";
    }

    String loginUsername = principal.getName();
    String type = "board";

    if (boardId == null) { // 새 글 작성
        Long newBoardId = boardService.save(board, loginUsername, type); // Insert

        // 첨부파일 있을 때
        if (!files.get(0).getOriginalFilename().isEmpty()) {
            for (int i = 0; i < files.size(); i++) {
                if (files.get(i).getContentType().contains("image/")) {
                    fileService.saveFile(files.get(i), newBoardId);
                } else {
                    System.out.println("이미지 타입이 아닙니다");
                }
            }
        }
    } else { // 기존 글 수정
        boardService.update(board, boardId, type); // Update
    }

    return "redirect:/board/list";
}
  • 이 전글에선 @Valid를 사용했지만 이번 글에서는 Javascript를 이용해 View에서 제목과 내용의 유무를 체크할 예정입니다. 물론 첫 줄의 검증 코드로 어느정도 검증을 진행! (Valid를 통해서 값을 검증하니 여러 오류가 발생했어요.. 이에 대한 대체 방안입니다,,,)
  • 작성(create), 수정(update)를 하나의 Controller와 View에서 처리하기 때문에 boardId(게시판 id)가 null인 상태와 null이 아닌 상태를 나눠서 처리했습니다.
  • type : 향후 다른 종류 게시판을 만들거나 공지사항 추가 등 서비스의 확장을 고려해 추가했습니다.

 

4. Service

// id를 이용해서 해당 글 수정
public Board contentLoad(Long id, String type) {
    Board board = new Board();
    board.setId(id);
    board.setType(type);
    try {
        board = boardRepository.findById(board);
    } catch (Exception e) {
        System.out.println("boardService.contentLoad() .. error : " + e.getMessage());
    } finally {
        return board;
    }
}
  • GetMapping에서 사용되는 contentLoad입니다.
// 글 등록
public Long save(Board board, String loginUsername, String type) {
    board.setCreateDate(Timestamp.valueOf(LocalDateTime.now()));

    try {
        Member member = memberService.getMember(loginUsername);

        board.setWriterId(member.getId());
        board.setWriter(member.getUsername());
        board.setType(type);

        boardRepository.insertBoard(board);
    } catch (Exception e) {
        System.out.println("boardService.save() .. error : " + e.getMessage());
    }
    return board.getId();
}
// 글 수정
public Long update(Board board, Long boardId, String type) {
    board.setId(boardId);
    board.setType(type);

    try {
        boardRepository.updateBoard(board);
    } catch (Exception e) {
        System.out.println("boardService.update() .. error : " + e.getMessage());
    }
    return board.getId();
}

 

5. Mapper.xml

<!--특정 아이디 게시글 조회-->
<select id="findById" resultType="Board">
    SELECT
        <include refid="boardColumns"/>
    FROM
        tb_board
    WHERE
        id = #{id} AND type = #{type}
</select>
  • type : board, notice 등으로 조회합니다. (해당 포스트 같은 경우 board입니다.)
  • 만약 공지사항을 조회한다면 notice가 들어갈 것입니다.

 

<!--게시글 작성-->
<insert id="insertBoard" parameterType="Board" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO
         tb_board (title, content, writer_id, writer, create_date, type)
    VALUES
        (#{title}, #{content}, #{writerId}, #{writer}, #{createDate}, #{type})
</insert>

<!--게시글 업데이트-->
<update id="updateBoard" parameterType="Board" useGeneratedKeys="true" keyProperty="id">
    UPDATE
        tb_board
    SET
        title = #{title}
        ,content = #{content}
    WHERE
        id = #{id} AND type = #{type}
</update>
  • useGenratedKeys, keyProperty : insert와 update 시 결과값으로 완료된 row의 pk(id)를 반환받습니다.

 

6. View

			<form action="#" th:action="@{/board/form}" th:object="${board}" method="post" enctype="multipart/form-data"
				onsubmit="return checkForm(this);">
				<input type="hidden" name="boardId" th:value="*{id}">
				<input type="hidden" th:field="*{writerId}">
				<div class="mb-3">
					<label for="title" class="form-label">제목</label>
					<input type="text" class="form-control" id="title" th:field="*{title}">
				</div>
				<div class="mb-3">
					<label for="content" class="form-label">내용</label>
					<textarea class="form-control" id="content" rows="13" th:field="*{content}"></textarea>
				</div>

				<div id="imageThumbnail">
				</div>

				<!-- button -->
				<th:block th:if="${param.boardId} == null">
					<div id="uploadForm">
						<div id="uploadElement">
							<input id="uploadInput" type="file" class="btn btn-outline-primary" name="files"
								accept="image/*" onchange="setThumbnail(event);" multiple /> <span
								style="font-size: small;"> * jpeg / png 타입의
								이미지는
								최대 7개까지
								등록해주세요.</span>
						</div>
						<a id="reset" class="mt-3 btn btn-danger" onclick="resetImg()">초기화</a>
					</div>
				</th:block>


				<div class="nav justify-content-end mb-5">
					<button id="submit" type="submit" class="me-2 btn btn-primary">작성</button>
					<a type="button" class="btn btn-primary" th:href="@{/board/list}">나가기</a>
				</div>
			</form>
  • th:object - form을 제출할 때 설정된 객체로 리턴됩니다. (이 경우 컨트롤러의 Board)
  • th:field - 해당 값(name과 value)을 th:object에서 설정된 객체의 내부 값으로 설정됩니다. (Board.setId, Board.setTitle...등)
  • onsubmit : form 태그의 값을 서버로 전송하기 전에 실행하여 유효성을 체크합니다. 

 

7. form검증을 위한 script (파일 처리부분은 이 전글 참고)

		// form submit 검증
		function checkForm(form) {
			if (form.title.value == "") {
				alert('제목을 입력하세요.');
				form.title.classList.add('is-invalid');
				form.title.focus();
				return false;
			}

			if (form.content.value == "") {
				alert('내용을 입력하세요.');
				form.title.classList.remove('is-invalid');
				form.content.classList.add('is-invalid');
				form.content.focus();
				return false;
			}
			form.content.classList.remove('is-invalid');
			return true;
		}
  • onsubmit에서 this를 통해 받았기 때문에 form.[name]으로 element를 지정가능!
  • 제목이나 내용이 없을 경우 경고 팝업을 띄워주고, 해당 input 또는 textarea의 class에 is-invalid를 추가해줍니다. (bootstrap을 이용해 빨간 input칸으로 변화)
  • false를 return 받는 경우는 서버로 form 값이 제출 안됩니다.

 

8. 시연

 

728x90

Entity 클래스에 @Id, @Size, @NotBlank 등을 붙여주고 Controller 클래스에서 @Valid 어노테이션을 추가적으로 사용함으로써 기본적인 검증은 가능하다.

 

1. 기본 form 입력값 검증하기

@Data
@Entity
@DynamicUpdate
@DynamicInsert
@Table(name = "tb_userinfo")
@IdClass(MemberID.class)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Id
    @NotNull
    @Pattern(regexp = "^[a-z0-9]*$", message = "username은 영문(소문자)과 숫자만 가능합니다.")
    @Size(min = 5, max = 20, message = "username은 5자 이상 20자 이하입니다.")
    private String username;

    @NotNull
    @Size(min = 6, max = 100, message = "암호는 6자 이상 100자 이하 입니다.")
    private String password;

    @NotNull
    @Size(min = 6, max = 45, message = "올바른 이메일을 입력하세요.")
    @Email(message = "이메일 형식을 맞춰주세요.")
    private String email;

    @Column
    private String role;
}
  • 각 필드별로 제약조건 어노테이션을 추가해준다.
@GetMapping("/joinForm")
public String joinForm(Model model) {
    model.addAttribute("member", new Member());
    return "/account/joinForm";
}

@PostMapping("/join")
public String join(@Valid Member member, BindingResult bindingResult){ // view의 form->input 의 name과 매핑됨.
    if(bindingResult.hasErrors()) {
        return "/account/joinForm";
    }

    String encPwd = memberService.pwdEncoding(member.getPassword());
    member.setPassword(encPwd);

    memberService.join(member);

    return "redirect:/loginForm";
}
  • GetMapping으로 처음 회원가입 화면으로 진입할 때 Member 객체를 만들어서 member라는 키값으로 전달해준다.
  • PostMapping에서는 View에서 데이터를 담아 전달해준 member 객체에 @Valid 어노테이션을 달아준다.
  • BindingResult.hasErrors()를 통해 기본적인 에러 유무를 알아 낼 수 있고, 다시 회원가입 화면으로 돌려보낸다.
<form th:action="@{/join}" method="post" th:object="${member}">          
          <div class="form-floating mt-4">
            <input type="text" class="form-control" th:field="*{username}" th:classappend="${#fields.hasErrors('username')} ? 'is-invalid'" id="floatingInput" placeholder="Username">
            <label for="floatingInput">Username</label>
            <div th:if="${#fields.hasErrors('username')}" th:errors="*{username}" id="validationServer03Feedback" class="invalid-feedback text-start">
              Username Error
            </div>
          </div>
          <div class="form-floating">
            <input type="password" class="form-control mt-2" th:field="*{password}" th:classappend="${#fields.hasErrors('password')} ? 'is-invalid'" id="floatingPassword" placeholder="Password">
            <label for="floatingPassword">Password</label>
            <div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" id="validationServer03Feedback" class="invalid-feedback text-start" style="margin-bottom: 8px;">
              Password Error
            </div>
          </div>
          <div class="form-floating">
            <input type="email" class="form-control" th:field="*{email}" th:classappend="${#fields.hasErrors('email')} ? 'is-invalid'" id="floatingPassword" placeholder="Email">
            <label for="floatingPassword">example@board.com</label>
            <div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" id="validationServer03Feedback" class="invalid-feedback text-start" style="margin-top: 8px;">
              Email Error
            </div>
          </div>
      
          <div class="checkbox mb-3">
          </div>
          <button class="w-100 btn btn-lg btn-primary mt-2" type="submit">Get started!</button>
          <a type="button" class="w-100 btn btn-lg btn-primary mt-2" th:href="@{/}">exit</a>
        </form>
  • 타임리프의 th:if가 달린 3개의 div 태그에서 에러를 출력해준다.
  • th:if=${#fields.hasErrors('필드명')} : 에러가 발생한 경우 true 값을 반환하여 div태그를 표시해준다.
  • form 태그의 th:object="{member}" : GetMapping 때 전달받은 Member의 키 값을 넣어 전달 받은 객체
  • "*{username}" :  위 설명을 참고해서 Member.getUsername()을 View에서 사용할 수 있도록 한 값이다. -> ${member.username} 과 같다고 생각!
  • th:errors="*{password}" : password에 대한 에러 내용을 표시해준다.

+ Recent posts