본문 바로가기
Spring Boot

스프링 부트(Spring Boot) 게시판 - 게시글 리스트 조회 기능 구현하기 [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBatis]

by 도뎡 2023. 4. 5.
반응형

  • 본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다.
  • DBMS 툴은 DBeaver를 이용하며, DB는 MariaDB를 이용합니다. (MariaDB 설치하기)
  • 화면 처리는 HTML5 기반의 자바 템플릿 엔진인 타임리프(Thymeleaf)를 사용합니다.

이전 글에서는 게시글 데이터를 처리할 서비스, 컨트롤러, 그리고 게시글 등록 페이지(HTML)와 게시판의 모든 페이지에서 공통으로 사용할 레이아웃까지 처리해 보았습니다.

 

이번에는 게시글의 목록을 보여줄 리스트 페이지를 구현해 볼 건데요. 우리는 이미 서비스(Service)와 매퍼(Mapper)에 게시글 CRUD 기능을 모두 구현해 두었기 때문에 화면(User Interface)을 담당하는 컨트롤러와 HTML만 손봐주면 됩니다.

 

1. 컨트롤러(Controller)에 메서드 추가하기

이전 글에서 생성한 PostController에 다음의 메서드를 추가해 주세요.

    // 게시글 리스트 페이지
    @GetMapping("/post/list.do")
    public String openPostList(Model model) {
        List<PostResponse> posts = postService.findAllPost();
        model.addAttribute("posts", posts);
        return "post/list";
    }

 

1) @GetMapping

GET 방식의 HTTP 요청 메서드를 의미합니다. 이전에 말씀드렸듯이, 데이터를 조회하거나 화면을 리턴하는 경우에는 GET 방식을 이용합니다.

 

 

2) posts

PostService의 findAllPost( )의 실행 결과를 담은 게시글 리스트 객체입니다. Model 인터페이스의 addAttribute( )를 이용해 "posts"라는 이름으로 리스트 데이터를 화면(HTML)으로 전달합니다.

 

 

3) return 문

컨트롤러의 리턴 타입이 String이면, 리턴 문에 선언된 경로의 HTML이 화면에 출력됩니다. openPostList( )의 "post/list"는 src/main/resources/templates/post/list.html을 의미합니다.

 

 

2. 화면(HTML) 생성하기

이번에는 openPostList( )의 리턴 문에 선언된 list.html을 추가해 줄 차례입니다. 게시글 등록 페이지(write.html)와 동일한 경로에 list.html을 추가하고, 소스 코드를 작성해 주세요.

 

타임리프의 th 속성, 네임스페이스 선언, 레이아웃 파일 경로 선언에 대한 내용은 분량이 적지 않기 때문에 여기서는 생략하도록 하겠습니다. 혹시라도 궁금하신 분들께서는 이전 글을 참고해 주세요!

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="layout/basic">
    <th:block layout:fragment="title">
        <title>리스트 페이지</title>
    </th:block>

    <th:block layout:fragment="content">
        <div class="page_tits">
            <h3>게시판 관리</h3>
            <p class="path"><strong>현재 위치 :</strong> <span>게시판 관리</span> <span>리스트형</span> <span>리스트</span></p>
        </div>

        <div class="content">
            <section>
                <!--/* 검색 */-->
                <div class="search_box">
                    <form id="searchForm" onsubmit="return false;" autocomplete="off">
                        <div class="sch_group fl">
                            <select title="검색 유형 선택">
                                <option value="">전체 검색</option>
                                <option value="">제목</option>
                                <option value="">내용</option>
                            </select>
                            <input type="text" placeholder="키워드를 입력해 주세요." title="키워드 입력"/>
                            <button type="button" class="bt_search"><i class="fas fa-search"></i><span class="skip_info">검색</span></button>
                        </div>
                    </form>
                </div>
              
                <!--/* 리스트 */-->
                <table class="tb tb_col">
                    <colgroup>
                        <col style="width:50px;"/><col style="width:7.5%;"/><col style="width:auto;"/><col style="width:10%;"/><col style="width:15%;"/><col style="width:7.5%;"/>
                    </colgroup>
                    <thead>
                        <tr>
                            <th scope="col"><input type="checkbox"/></th>
                            <th scope="col">번호</th>
                            <th scope="col">제목</th>
                            <th scope="col">작성자</th>
                            <th scope="col">등록일</th>
                            <th scope="col">조회</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr th:if="${not #lists.isEmpty( posts )}" th:each="row, status : ${posts}">
                            <td><input type="checkbox"/></td>
                            <td th:text="${row.noticeYn == false ? (status.size - status.index) : '공지'}"></td>
                            <td class="tl"><a th:href="@{/post/view.do( id=${row.id} )}" th:text="${row.title}"></a></td>
                            <td th:text="${row.writer}"></td>
                            <td th:text="${#temporals.format( row.createdDate, 'yyyy-MM-dd HH:mm' )}"></td>
                            <td th:text="${row.viewCnt}"></td>
                        </tr>
    
                        <tr th:unless="${not #lists.isEmpty( posts )}">
                            <td colspan="5">
                                <div class="no_data_msg">검색된 결과가 없습니다.</div>
                            </td>
                        </tr>
                    </tbody>
                </table>

                <!--/* 페이지네이션 */-->
                <div class="paging">
                    <a href="#" class="page_bt first">첫 페이지</a><a href="#" class="page_bt prev">이전 페이지 그룹</a>
                    <p><span class="on">1</span><a href="#">2</a><a href="#">3</a><a href="#">4</a><a href="#">5</a><a href="#">6</a><a href="#">7</a><a href="#">8</a><a href="#">9</a><a href="#">10</a></p>
                    <a href="#" class="page_bt next">다음 페이지 그룹</a><a href="#" class="page_bt last">마지막 페이지</a>
                </div>

                <!--/* 버튼 */-->
                <p class="btn_set tr">
                    <a th:href="@{/post/write.do}" class="btns btn_st3 btn_mid">글쓰기</a>
                </p>
            </section>
        </div> <!--/* .content */-->
    </th:block>
</html>

 

구성 요소 설명
div.search_box 게시글 목록에서 특정 게시글을 검색할 수 있는 영역입니다. 아직은 검색 기능을 구현하지 않았기 때문에 아무런 작동을 하지 않습니다. CRUD 기능이 모두 마무리되면 페이징 처리와 같이 진행될 예정입니다.
table.tb tb_col 게시글 작성 페이지(write.html)와 마찬가지로, 리스트 페이지의 실제 컨텐츠가 들어가는 영역입니다. 리스트 데이터는 보통 테이블(table) 형태로 처리되며, 타임리프의 속성이 사용된 부분을 위주로 알아보도록 하겠습니다.
div.paging 리스트에서 특정 페이지로 이동할 수 있는 페이지네이션(페이지 번호) 영역입니다. 검색 영역과 마찬가지로 CRUD 기능 구현이 모두 마무리되면 검색과 함께 진행될 예정입니다. 지금은 번호를 클릭해도 작동하지 않는 것이 정상입니다.

 

 

 

다음은 게시글 리스트 데이터가 출력되는 영역입니다.

리스트 데이터 출력 영역

 

구성 요소 설명
th:if와 th:unless 46번 라인의 th:if는 우리가 흔히 알고 있는 if 문과 동일하고, 55번 라인의 th:unless는 else 문과 같다고 볼 수 있습니다. 하지만, th:unless는 일반적인 언어의 else 문과는 달리 th:if와 동일한 조건을 지정해 주어야 합니다. (처음에는 살짝 혼란이 올 수 있습니다.)

결과적으로 th:if 조건이 성립되면 게시글 리스트가 출력되고, th:unless의 조건이 성립되면 "검색된 결과가 없습니다." 라는 메시지가 출력됩니다.
#lists.isEmpty 다음은 th:if에 들어간 조건입니다. 해당 함수는 인자로 전달하는 리스트 데이터가 비어있는지 확인하는 데 사용됩니다. isEmpty( ) 함수는 데이터가 비어있으면 true를 리턴하는데요. 함수 앞의 "not"은 부정을 의미하며, ' ! ' 또는 #lists.isEmpty( posts ) == false 등 여러분이 지향하시는 스타일로 코딩해 주시면 되겠습니다.

참고로 인자에 공백을 넣은 이유는 가독성 측면에서 넣은 것이며, 붙여서 사용하셔도 무관합니다.
th:each th:each는 JSTL의 <c:forEach>, 자바의 forEach와 유사한 기능입니다. 여기서는 "row"라는 이름으로 posts(리스트 객체)를 순환하며 데이터를 출력합니다.
첫 번째 <td>

일반적인 체크박스입니다. 추후에 여러 데이터를 선택해서 데이터 다중 삭제를 구현할 때 사용합니다.
두 번째 <td>

공지 여부가 false(0)로 설정된 게시글이라면 (전체 데이터 수 - 현재 행(row)의 인덱스) 즉, 현재 행(row)의 번호를 출력하고, true(1)로 설정된 게시글이라면 "공지"를 출력합니다.
세 번째 <td>

제목을 클릭하면, 게시글 상세 페이지("/post/view.do")의
URI를 호출합니다. 보통 href 속성에 쿼리 스트링 파라미터를 연결할 때 첫 번째 파라미터는 ' ? '로 연결하고, 두 번째부터는 ' & '로 연결하는데요. 타임리프는 URI 뒤에 괄호를 열어 파라미터를 연결합니다.


일반적인 GET 파라미터 연결 방식 -▶ /post/view.do?idx=${idx}&page=${page}

타임리프 GET 파라미터 연결 방식 - /post/view.do( idx=${idx}, page=${page} )
네 번째 <td>

작성자를 텍스트 형식으로 출력합니다.
다섯 번째 <td>

날짜 타입의 멤버 변수는 temporals.format( ) 함수를 이용해서 원하는 날짜 형태로 포맷할 수 있습니다. 해당 태그는 게시글 생성 일시를 "연-월-일 시:분" 형태로 출력합니다. 만약, 초까지 출력하고 싶다면 "yyyy-MM-dd HH:mm:ss"로 포맷해 주시면 됩니다.
여섯 번째 <td>

게시글 조회 수를 텍스트 형식으로 출력합니다.
글쓰기 버튼 "/post/write.do" URI를 호출해서 신규 게시글 작성 페이지로 이동합니다.

 

 

3. 애플리케이션 실행하기

애플리케이션을 실행하고 게시글 리스트 페이지에 접속해 보면, 이전에 등록한 데이터가 출력되는 것을 확인하실 수 있습니다. (이미지를 클릭하면 확대해 보실 수 있습니다.)

게시글 리스트 페이지

 

 

마치며

이번에는 게시글 리스트 페이지를 구현해 보았습니다. 이제 CRUD에서 게시글 상세정보 조회 기능과 게시글 삭제 기능만이 남았습니다.

 

CRUD 처리가 모두 마무리되면, 로그와 인터셉터 등 스프링에서 빠질 수 없는 필수적인 기능들을 프로젝트에 적용하고, 페이징과 검색 처리, REST API 방식의 댓글 처리, 마지막으로 게시글에 첨부 파일을 업로드하는 방법을 알려드리고자 합니다.

 

오늘도 방문해 주신 여러분께 진심으로 감사드리고, 다음 글에서 뵙겠습니다! 좋은 하루 보내세요 :)

 

Board.zip
0.65MB

반응형

댓글