Spring/SpringBoot

Spring Boot으로 웹 출시 - thymeleaf에서 form 전송

느리지만 꾸준하게 2022. 6. 2. 12:08
  • Spring Boot, thymeleaf를 이용해서 form 전송 방법 알아보기

 

  • form 유효값 체크할 수 있는 Validator 작성하기

 

  • JPA를 이용해서 DB에 데이터 추가, 수정

 

 

Bootstrap으로 form 검색하고 form.html파일을 만들어준다.

 

form.html

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<head th:replace="fragments/common :: head('게시판')">
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
          integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
    <link href="starter-template.css" th:href="@{/starter-template.css}" rel="stylesheet">

    <title>게시판</title>
</head>

<body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top" th:replace="fragments/common :: menu('board')">
</nav>
<div class="container">
    <h2>게시판</h2>
    <form action="#" th:action="@{/board/form}" th:object="${board}" method="post">
        <input type="hidden" th:field="*{id}">
        <div class="form-group">
            <label for="title">제목</label>
            <input type="email" class="form-control"
                   th:classappend="${#fields.hasErrors('title')} ? 'is-invalid'" id="title" th:field="*{title}">
            <div class="invalid-feedback" th:if="${#fields.hasErrors('title')}" th:errors="*{title}">
                제목 에러 메시지
            </div>
        </div>

        <div class="form-group">
            <label for="content">내용</label>
            <textarea class="form-control" id="content" rows="3" th:field="*{content}"
                      th:classappend="${#fields.hasErrors('content')} ? 'is-invalid'"></textarea>
            <div class="invalid-feedback" th:if="${#fields.hasErrors('content')}" th:errors="*{content}">
                제목 에러 메시지
            </div>
        </div>
        <div class="text-right">
            <a class="btn btn-primary" th:href="@{/board/list}">취소</a>
            <button type="button" class="btn btn-primary"
                    sec:authorize="hasRole('ROLE_ADMIN')" th:onclick="|deleteBoard(*{id})|">삭제</button>
            <button type="submit" class="btn btn-primary">확인</button>
        </div>
    </form>
</div>

<footer th:replace="fragments/common :: footer"></footer>
<script>
        function deleteBoard(id) {
            //DELETE /api/boards/{id}
            $.ajax({
                url: '/api/boards/' + id,
                type: 'DELETE',
                success: function(result) {
                    console.log('result', result);
                    alert('삭제됐습니다.');
                    window.location.href = '/board/list';
                }
            });
        }
    </script>
</body>

</html>

 

list.html은 아래와 같다.

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">

<head th:replace="fragments/common :: head('게시판')">
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
        integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
    <link href="starter-template.css" th:href="@{/starter-template.css}" rel="stylesheet">

    <title>게시판</title>
</head>

<body>
    <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top" th:replace="fragments/common :: menu('board')">
        <!-- <a class="navbar-brand" href="#">Spring Boot Tutorial</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault"
            aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarsExampleDefault">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item active">
                    <a class="nav-link" href="#">홈 <span class="sr-only">(current)</span></a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">게시판</a>
                </li>
            </ul>            
        </div> -->
    </nav>

    <div class="container">
        <h2>게시판</h2>
        <div>총 건수 : <span th:text="${boards.totalElements}"></span></div>
        <form class="form-inline d-flex justify-content-end" method="GET" th:action="@{/board/list}">
            <div class="form-group mx-sm-3 mb-2">
                <label for="searchText" class="sr-only">검색</label>
                <input type="text" class="form-control" id="searchText" name="searchText"
                    th:value="${param.searchText}">
            </div>
            <button type="submit" class="btn btn-light mb-2">검색</button>
        </form>
        <table class="table table-striped">
            <thead>
                <tr>
                    <th scope="col">번호</th>
                    <th scope="col">제목</th>
                    <th scope="col">작성자</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="board : ${boards}">
                    <td th:text="${board.id}">Mark</td>
                    <td><a th:text="${board.title}" th:href="@{/board/form(id=${board.id})}">Otto</a></td>
                    <td th:text="${board.user.username}">홍길동</td>
                </tr>
            </tbody>
        </table>
        <nav aria-label="Page navigation example">
            <ul class="pagination justify-content-center">
                <li class="page-item" th:classappend="${1 == boards.pageable.pageNumber + 1} ? 'disabled'">
                    <a class="page-link" href="#"
                        th:href="@{/board/list(page=${boards.pageable.pageNumber - 1},searchText=${param.searchText})}"
                        tabindex="-1" aria-disabled="true">Previous</a>
                </li>
                <li class="page-item" th:classappend="${i == boards.pageable.pageNumber + 1} ? 'disabled'" th:each="i : ${#numbers.sequence(startPage, endPage)}">
                  <a class="page-link" href="#" th:href="@{/board/list(page=${i - 1},searchText=${param.searchText})}" th:text="${i}">1</a>
                </li>
              <li class="page-item" th:classappend="${boards.totalPages == boards.pageable.pageNumber + 1} ? 'disabled'">
                <a class="page-link" href="#" th:href="@{/board/list(page=${boards.pageable.pageNumber + 1},searchText=${param.searchText})}">Next</a>
              </li>
            </ul>
          </nav>
          <div class="text-right">
            <a type="button" class="btn btn-primary" th:href="@{/board/form}">쓰기</a>
        </div>
    </div>

    <footer th:replace="fragments/common :: footer"></footer>
</body>

</html>

 

 

 

이제 form.html과 controller을 연결시켜주자.

package com.example.myhome.controller;


import com.example.myhome.model.Board;
import com.example.myhome.repository.BoardRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@Controller
@RequestMapping("/board")
public class BoardController {

    // DI => 서버 기동될 때 인스턴스가 들어온다.
    @Autowired
    private BoardRepository boardRepository;

    @GetMapping("/list")
    public String list(Model model) {
        List<Board> boards = boardRepository.findAll();
        // boards 라는 key값에 boards 라는 데이터를 준다.
        // 모델에 담긴 데이터를 thymeleaf를 통해서 사용할 수 있다.
        model.addAttribute("boards", boards);
        return "board/list";
    }
    
    // form과 연결 시켜준다.
    @GetMapping("/form")
    public String form(Model model) {
        return "board/form";
    }
}

 

 

 

Controller를 만드는 방법 / Form을 어떻게 전송하면 되는지를 spring.io에서 확인해주자.

 

Handling Form Submission

this guide is designed to get you productive as quickly as possible and using the latest Spring project releases and techniques as recommended by the Spring team

spring.io

 

 

 

위 사이트를 참고하여 boardController를 세팅을 해준다.

package com.example.myhome.controller;


import com.example.myhome.model.Board;
import com.example.myhome.repository.BoardRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@Controller
@RequestMapping("/board")
public class BoardController {

    // DI => 서버 기동될 때 인스턴스가 들어온다.
    @Autowired
    private BoardRepository boardRepository;

    @GetMapping("/list")
    public String list(Model model) {
        List<Board> boards = boardRepository.findAll();
        model.addAttribute("boards", boards);
        return "board/list";
    }

    @GetMapping("/form")
    public String form(Model model) {
        model.addAttribute("board", new Board());
        return "board/form";
    }

    @PostMapping("/form")
    public String greetingSubmit(@ModelAttribute Board board) {
        boardRepository.save(board);
        return "redirect:/board/result";
    }
}

 

그리고 id에 값이 있을 경우에는 조회를 해주고 없을 경우에는 null을 넘겨주는 코드를 작성해준다.

// BoardController Class

GetMapping("/form")
    public String form(Model model, @RequestParam(required = false) Long id) {
        if(id == null) {
            model.addAttribute("board", new Board());
        } else {
            Board board = boardRepository.findById(id).orElse(null);
            model.addAttribute("board", board);
        }

        return "board/form";
    }

    @PostMapping("/form")
    public String greetingSubmit(@ModelAttribute Board board) {
        boardRepository.save(board);
        return "redirect:/board/result";
    }

 

 

Model Package에 Board Class도 작성해준다.

package com.example.myhome.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Entity
@Data
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @NotNull
    @Size(min = 2, max = 30, message = "제목은 2자이상 30자 이하입니다.")
    private String title;
    private String content;
}

 

 

spring boot validator 사이트도 참고를 해준다.

 

 

 

 

 

validator 패키지도 만들어주고 BoardValidator Class도 만들어주자.

 

package com.example.myhome.validator;

import com.example.myhome.model.Board;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

public class BoardValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Board.class.equals(clazz);
    }

    @Override
    public void validate(Object obj, Errors errors) {
        Board b = (Board) obj;
        if(StringUtils.isEmpty(b.getContent())) {
            errors.rejectValue("content", "key", "내용을 입력하세요");
        }
    }
}

 

 

controller package에서 BoardController Class를 만들어주자.

스프링이 기동될 때 인스턴스가 담긴다.

@Controller
@RequestMapping("/board")
public class BoardController {

    // DI => 서버 기동될 때 인스턴스가 들어온다.
    @Autowired
    private BoardRepository boardRepository;

    @Autowired
    private BoardValidator boardValidator;
    
    
    @GetMapping("/form")
    public String form(Model model, @RequestParam(required = false) Long id) {
        if(id == null) {
            model.addAttribute("board", new Board());
        } else {
            Board board = boardRepository.findById(id).orElse(null);
            model.addAttribute("board", board);
        }

        return "board/form";
    }

    @PostMapping("/form")
    public String greetingSubmit(@Valid Board board, BindingResult bindingResult) {
        boardValidator.validate(board, bindingResult);
        if (bindingResult.hasErrors()) {
            return "board/form";
        }
        boardRepository.save(board);
        return "redirect:/board/result";
    }

 

BoardValidator에서도 Component라고 명시를 해주자.

@Component
public class BoardValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Board.class.equals(clazz);
    }

 

 

 

 

 

 

 

 

<참고 :Spring Boot으로 웹 출시까지 #6. thymeleaf에서 form 전송하기>

https://www.youtube.com/watch?v=nx5UuyS8foI&list=PLPtc9qD1979DG675XufGs0-gBeb2mrona&index=7