Spring

스프링부트 게시판 만들기 - 5 (Thymeleaf를 사용하여 게시글 작성하기 Create)

it's woo 2022. 6. 30. 04:36

Controller를 만들어 CRUD의 C(Create)를 구현할 것이다.

 

BoardController를 만들고 초기 화면과 게시글 작성 화면을 만들어보자.

 

BoardController

@Controller
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

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

    @GetMapping("/boardForm")
    public String boardForm(){
        return "board/boardForm";
    }

    @PostMapping("/boardForm")
    public String boardSummit(@ModelAttribute("board") Board board){
        boardService.save(board);
        return "redirect:/";
    }
}

@GetMapping("/")

데이터베이스의 모든 데이터를 List로 받아 model에 넘겨 출력시켜줍니다.

 

@GetMapping("/boardForm")

게시글 양식을 화면에 출력합니다.

 

@PostMapping("/boardForm")

입력받은 정보로 게시글을 만들어 저장합니다.

 

부트스트랩에서 복사해온 html 파일에 겹치는 부분은 header, bodyHeader로 관리하여 중복을 줄이자

 

header.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="header">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
    <meta name="generator" content="Hugo 0.98.0">
    <title>Headers · Bootstrap v5.2</title>
    <link rel="canonical" href="https://getbootstrap.com/docs/5.2/examples/headers/">
    <link href="/css/bootstrap.min.css" rel="stylesheet">

    <style>
        .bd-placeholder-img {
            font-size: 1.125rem;
            text-anchor: middle;
            -webkit-user-select: none;
            -moz-user-select: none;
            user-select: none;
        }

        @media (min-width: 768px) {
            .bd-placeholder-img-lg {
                font-size: 3.5rem;
            }
        }

        .b-example-divider {
            height: 3rem;
            background-color: rgba(0, 0, 0, .1);
            border: solid rgba(0, 0, 0, .15);
            border-width: 1px 0;
            box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
        }

        .b-example-vr {
            flex-shrink: 0;
            width: 1.5rem;
            height: 100vh;
        }

        .bi {
            vertical-align: -.125em;
            fill: currentColor;
        }

        .nav-scroller {
            position: relative;
            z-index: 2;
            height: 2.75rem;
            overflow-y: hidden;
        }

        .nav-scroller .nav {
            display: flex;
            flex-wrap: nowrap;
            padding-bottom: 1rem;
            margin-top: -1px;
            overflow-x: auto;
            text-align: center;
            white-space: nowrap;
            -webkit-overflow-scrolling: touch;
        }
    </style>
    <!-- Custom styles for this template -->
    <link href="/css/headers.css" rel="stylesheet">
</head>

 

bodyHeader.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div class="header" th:fragment="bodyHeader">
    <header class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between py-3 mb-4 border-bottom">
        <a href="/" class="d-flex align-items-center col-md-3 mb-2 mb-md-0 text-dark text-decoration-none">
            <svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap">
                <use xlink:href="#bootstrap"></use>
            </svg>
        </a>
        <ul class="nav col-12 col-md-auto mb-2 justify-content-center mb-md-0">
            <li><a href="#" class="nav-link px-2 link-secondary">Home</a></li>
            <li><a href="#" class="nav-link px-2 link-dark">Features</a></li>
            <li><a href="#" class="nav-link px-2 link-dark">Pricing</a></li>
            <li><a href="#" class="nav-link px-2 link-dark">FAQs</a></li>
            <li><a href="#" class="nav-link px-2 link-dark">About</a></li>
        </ul>
        <div class="col-md-3 text-end">
            <button type="button" class="btn btn-outline-primary me-2">Login</button>
            <button type="button" class="btn btn-primary">Sign-up</button>
        </div>
    </header>
</div>

 

원래 header나 bodyHeader 부분은 다음과 같이 대체할 수 있다.

//fragments디렉토리에 header파일로 대체
<head th:replace="fragments/header :: header" />
//fragments디렉토리에 bodyHeader파일로 대체
<div th:replace="fragments/bodyHeader :: bodyHeader"/>

이렇게 하여 코드가 훨씬 깔끔해지고 화면 구성에 집중할 수 있다.

 

BoardList.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />

<body>
<main>
    <div class="container">
        <div th:replace="fragments/bodyHeader :: bodyHeader"/>
    </div>
</main>

<div class = "container">
    <table class="table">
        <thead>
        <tr>
            <th>글 번호</th>
            <th>제목</th>
            <th>작성자</th>
            <th>조회</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each = "board : ${boardList}">
            <td th:text="${board.id}"></td>
            <td th:text="${board.title}"></td>
            <td th:text="${board.writer}"></td>
            <td th:text="${board.view}"></td>
        </tr>
        </tbody>
    </table>
    <a href="#" th:href="@{/boardForm}" class="btn btn-primary" role="button">글쓰기</a>

</div>

<script src="/js/bootstrap.bundle.min.js"></script>
</body>
</html>

 

BoardForm.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />

<body>
<main>
    <div class="container">
        <div th:replace="fragments/bodyHeader :: bodyHeader"/>
    </div>
</main>

<div class = "container">
    <div class = "row">
        <div class = "col-sm-12">
            <form th:action="@{/boardForm}"  method = "post">
                <div class = "form-group">
                    <label th:for="title">제목</label>
                    <input type="text" id = "title" name = "title" class = "form-control" placeholder="제목을 입력하세요">
                </div>
                <div class = "form-group">
                    <label th:for="writer">작성자</label>
                    <input type="text" id = "writer" name = "writer" class = "form-control" placeholder="작성자를 입력하세요">
                </div>
                <div class = "form-group">
                    <label th:for="content">내용</label>
                    <textarea id = "content" name = "content" class = "form-control"></textarea>
                </div>
                <button type = "submit" class="btn btn-primary">글쓰기</button>
            </form>
        </div>
    </div>
</div>


<script src="/js/bootstrap.bundle.min.js"></script>
</body>
</html>

 

Test는 MockMvc 객체를 사용하여 진행합니다.

 

BoardControllerTest

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
class BoardControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    BoardService boardService;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders
                .standaloneSetup(new BoardController(boardService))
                .build();
    }

    @Test
    void 초기화면() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

    @Test
    void 게시글_작성() throws  Exception {
        mockMvc.perform(MockMvcRequestBuilders.post("/boardForm")
                        .param("title","hi")
                        .param("writer","lee")
                        .param("content","hello"))
                .andExpect(MockMvcResultMatchers.status().is3xxRedirection());
    }

    @Test
    void 게시글_출력() throws Exception{
        mockMvc.perform(MockMvcRequestBuilders.get("/"))
                .andExpect(MockMvcResultMatchers.model().attributeExists("boardList"))
                .andExpect(MockMvcResultMatchers.status().isOk());

    }
}

처음 사용해본 MockMvc테스트라 잘했는지 모르겠다..

 

테스트를 완료하고 실제로 화면을 띄워보자

 

다음에는 게시글을 읽는 기능을 구현하여 보자