게시판 만들기 - 게시글 페이징 및 정렬 구현

정영찬·2022년 8월 28일
0

프로젝트 실습

목록 보기
38/60
post-thumbnail

게시글 페이징

게시판 페이지의 페이징 기능은 구현했지만, 게시글 페이지의 페이징 기능은 아직 구현하지 않았다.
게시판 페이지의 페이징 기능보다는 간단하다. 이전글과 다음글로 구성되어있으며, 각각의 버튼을 누르면 이전,다음 글로 이동하게 되는 것이다.

서비스 테스트 생성

게시글 수를 조회하면 게시글 수를 반환해주는 테스트를 생성한다.
마지막 게시글에 도달했을때 다음글이 존재하지 않으며, 맨 처음 게시글의 이전 글은 존재하지 않기 때문에 나는 전체 게시글의 수를 파악할 수 있는지를 테스트 해야한다.

@DisplayName("게시글 수를 조회하면, 게시글 수를 반환한다.")
    @Test
    void givenNothing_whenCountingArticles_thenReturnsArticleCount(){
        // Given
        Long expected = 0L;
        given(articleRepository.count()).willReturn(expected);

        // When
        long actual = sut.getArticleCount();
        // Then

       assertThat(actual).isEqualTo(expected);
       then(articleRepository).should().count();
    }

getArticleCount()는 아직 구현되지 않았다. 이는 articleService에 구현한다

    public long getArticleCount(){
        return articleRepository.count();
    }

컨트롤러 테스트 생성

컨트롤러 테스트중에서 게시글 페이지 정상호출 테스트에 게시글의 갯수도 파악하는 테스트까지 추가했다.

  @DisplayName("[view][GET] 게시글 페이지 - 정상 호출")
    @Test
    public void givenNothing_whenRequestingArticleView_thenReturnsArticleView() throws Exception {
        // Given
        Long articleId = 1L;
        Long totalCount = 1L;
        given(articleService.getArticle(articleId)).willReturn(createArticleCommentsDto());
        given(articleService.getArticleCount()).willReturn(totalCount);
        // When & Then
        mvc.perform(get("/articles/" + articleId))
                .andExpect(status().isOk()) // 정상 호출
                .andExpect(result -> content().contentType(MediaType.TEXT_HTML)) // 데이터 확인
                .andExpect(view().name("articles/detail")) // 뷰의 존재여부 검사
                .andExpect(model().attributeExists("article")) // 뷰에 모델 어트리뷰트로 넣어준 데이터존재 여부 검사
                .andExpect(model().attributeExists("articleComments"))
                .andExpect(model().attribute("totalCount",totalCount)); // getArticleCount()호출 여부
        then(articleService).should().getArticle(articleId);
        then(articleService).should().getArticleCount();
    }

컨트롤러에 어트리뷰트를 추가해줘야 테스트가 통과한다.


 @GetMapping("/{articleId}")
    public String article(@PathVariable Long articleId, ModelMap map) {
        ArticleWithCommentsResponse article = ArticleWithCommentsResponse.from(articleService.getArticle(articleId));
        map.addAttribute("article", article);
        map.addAttribute("articleComments", article.articleCommentsResponse());
        map.addAttribute("totalCount",articleService.getArticleCount());
        return "articles/detail";
    }

js

뷰에 적용시키기

이전에 적용시키지 않았던 #article-main과 #article-comment도 추가했고, #pagination 항목에 기능을 부여했다.
항상 이런 작업을 하면서 느끼는거지만, html 파일에서 기능을 부여하고 싶은 컴포넌트에 id지정이 잘 되어있는지 확인해서 오류가 발생하지 않도록 해야겠다고 생각한다.

<?xml version="1.0"?>
<thlogic xmlns:th="http://www.thymeleaf.org">
    <attr sel="#header" th:replace="header :: header"/>
    <attr sel="#footer" th:replace="footer :: footer"/>

    <attr sel="#article-main" th:object="${article}">
        <attr sel="#article-header/h1" th:text="*{title}"/>
        <attr sel="#nickname" th:text="*{nickname}"/>
        <attr sel="#email" th:text="*{email}"/>
        <attr sel="#created-at" th:datetime="*{createdAt}"
              th:text="*{#temporals.format(createdAt, 'yyyy-MM-dd HH:mm:ss')}"/>
        <attr sel="#hashtag" th:text="*{hashtag}"/>
        <attr sel="#article-content/pre" th:text="*{content}"/>


        <attr sel="#article-comments" th:remove="all-but-first">
            <attr sel="li[0]" th:each="articleComment : ${articleComments}">
                <attr sel="div/strong" th:text="${articleComment.nickname}"/>
                <attr sel="div/small/time" th:datetime="${articleComment.createdAt}"
                      th:text="${#temporals.format(articleComment.createdAt, 'yyyy-MM-dd HH:mm:ss')}"/>
                <attr sel="div/p" th:text="${articleComment.content}"/>
            </attr>
        </attr>

        <attr sel="#pagination">
            <attr sel="ul">
                <attr sel="li[0]/a"
                      th:href="*{id} - 1 <= 0 ? '#' : |/articles/*{id - 1}|"
                      th:class="'page-link' + (*{id} - 1 <= 0 ? ' disabled' : '')"
                />
                <attr sel="li[1]/a"
                      th:href="*{id} + 1 > ${totalCount} ? '#' : |/articles/*{id + 1}|"
                      th:class="'page-link' + (*{id} + 1 > ${totalCount} ? ' disabled' : '')"
                />
            </attr>

        </attr>

    </attr>


</thlogic>

현재 article-main 내부에 article-comment와 pagination이 둘다 존재하므로 article-main 내부에 모두 작성했다. 이때 pagination은 article-main에서 작성한 th:object="${article}" 을 사용해서 id값을 불러와 href에 사용한 것이다. 만약 id값에서 1을 뺀 값이 0보다 작거나 같으면 #처리를 해서 아무런 변화가 없게 하고, 그와 동시에 스타일 설정으로 disabled가 적용되어 클릭 할수 없게 만들었다.

반대로 다음글의 경우에는 id에서 1을 더한 값이 totalCount보다 커버리면 #처리를 하고 스타일 설정으로 disabled가 적용된다.


이제 상세글 페이지의 페이징 기능이 완벽하게 구현되었다.

정렬

게시판의 정렬 기능을 구현하다.
정렬 기능은 게시판 페이지에서 제목,해시태그,작성자,작성일을 클릭하면 해당 값으로 오른차순, 내림차순 정렬이 되게끔 동작하게 만드는 기능이다.

ArticleController를 만들때 articles매핑에서 articlespaginationBarNumber 어트리뷰트를 보내주는 articles는 Page로 구성되어있다. Page 에서는 정렬과 관련된 값들이 내재되어있으므로 이를 사용해서 정렬을 구현할수 있다

뷰에서 표현만 해주면 되기 때문에 뷰로 이동해서 내용을 작성한다.
index.html에 있는 table 요소중에서 th 내용을 클릭했을 때 기능이 동작하도록 구현해야한다.

				<th class="title col-6"><a>제목</a></th>
                <th class="hashtag col-2"><a>해시태그</a></th>
                <th class="user-id"><a>작성자</a></th>
                <th class="created-at"><a>작성일</a></th>

기능을 부여하는 디커플드 로직에 코드를 작성한다. #article-table 안에 작성하면 될것같다.

상위 목록에 object로 articles를 선언해놔야 articles와 관련된 요소를 사용할수 있기 때문에 바깥으로 main을 둘러쌓았다.

<attr sel="main" th:object="${articles}">
 <attr sel="#article-table">
            <attr sel="thead/tr">
                <attr sel="th.title/a" th:text="제목" th:href="@{/articles(
                page=${articles.number},
                sort='title' + (*{sort.getOrderFor('title')} != null ? (*{sort.getOrderFor('title').direction.name} != 'DESC' ? ',desc' : '') : '')
                )}" />
                <attr sel="th.hashtag/a" th:text="해시태그" th:href="@{/articles(
                page=${articles.number},
                sort='hashtag' + (*{sort.getOrderFor('hashtag')} != null ? (*{sort.getOrderFor('hashtag').direction.name} != 'DESC' ? ',desc' : '') : '')
                )}" />
                <attr sel="th.user-id/a" th:text="작성자" th:href="@{/articles(
                page=${articles.number},
                sort='userAccount.userId' + (*{sort.getOrderFor('userAccount.userId')} != null ? (*{sort.getOrderFor('userAccount.userId').direction.name} != 'DESC' ? ',desc' : '') : '')
                )}" />
                <attr sel="th.created-at/a" th:text="작성일" th:href="@{/articles(
                page=${articles.number},
                sort='createdAt' + (*{sort.getOrderFor('createdAt')} != null ? (*{sort.getOrderFor('createdAt').direction.name} != 'DESC' ? ',desc' : '') : '')
                )}" />
            </attr>
            
            ...
 <attr/>

여기서 작성자 항목에서 sort값만 userAccount.userId라고 되어있는데, 어트리뷰트로 가져오는 articles는 articleSrvice.searchArticles로 값을 가져온다.

Page<ArticleResponse> articles =  articleService.searchArticles(searchType,searchValue,pageable).map(ArticleResponse::from);

그럼 searchArticles는 제목,내용,id,닉네임,해시태그를 어디서 가져오느냐?

 public Page<ArticleDto> searchArticles(SearchType searchType, String searchKeyword, Pageable pageable){
        if(searchKeyword == null || searchKeyword.isBlank()) {
            return articleRepository.findAll(pageable).map(ArticleDto::from);
        }
        switch (searchType){
            case TITLE ->  articleRepository.findByTitleContaining(searchKeyword, pageable).map(ArticleDto::from);
            case CONTENT ->  articleRepository.findByContentContaining(searchKeyword, pageable).map(ArticleDto::from);
            case ID ->  articleRepository.findByUserAccount_UserIdContaining(searchKeyword, pageable).map(ArticleDto::from);
            case NICKNAME ->  articleRepository.findByUserAccount_NicknameContaining(searchKeyword, pageable).map(ArticleDto::from);
            case HASHTAG ->  articleRepository.findByHashtag("#" + searchKeyword, pageable).map(ArticleDto::from);
        };

        return Page.empty();
    }

articleRepository에서 findBy메소드로 값을 가져와서 mapping을 한다. ID의 경우 findByUserAccount_UserIdContaining 메소드를 호출하는데, 이 메소드는
ArticleRepository 내부에 작성한 Page<T>이다.

Page<Article> findByUserAccount_UserIdContaining(String userId, Pageable pageable);

page<Article>은 바로 Article 도메인을 바탕으로 작성되었고,

  @Setter @ManyToOne(optional = false) private UserAccount userAccount;

Article 도메인에서는,
userAccount에서 userId를 가져오기 때문에,

@Entity
public class UserAccount extends AuditingFields{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private  Long id;
    @Setter @Column(nullable = false, length = 50) private String userId;
    @Setter @Column(nullable = false) private String userPassword;
    @Setter @Column (length = 100) private String email;
    @Setter @Column (length = 100)private String nickname;
    @Setter private String memo;

그래서 작성자의 id값을 가져오기 위해서는 article 어트리뷰트에서 userAccount.userId를 가져와야한다.

이제 어플리케이션을 다시 실행해보면

이렇게 항목에 밑줄이 그어진 상태로 나타나면서 클릭을 하면 정렬을 해준다.

클릭을 하는 a 태그에 밑줄이 그어져 있어서 별로라면 스타일 속성을 설정해주면 된다.
articles/rable-header.css

/* 게시글 테이블 제목 */
#article-table > thead a {
    text-decoration: none;
    color: black;
}

text-decoration의 값을 none으로 해주면 a태그 내용의 밑줄이 사라진다. 이왕 하는 김에 텍스트 색도 변경했다.

정렬의 기능이 제대로 구현되었는지 확인을 위해서 테스트 데이터 추가했다. user_account를 하나 추가하고, 게시글 데이터중 2개의 user_account_id값을 2로 변경했다.
https://github.com/jyc-coder/bulletin-board/commit/26c2c65c912988b17c67a69d3b1f1f7aa3c1a9d3

댓글 기능 구현 작성 및 테스트 코드 수정

댓글 기능 구현을 해놓지 않아서 댓글 기능을 구현하고 테스트를 돌려 확인하는 과정에서 테스트 코드를 잘못 작성한 것이 발견되어 수정했다.
https://github.com/jyc-coder/bulletin-board/commit/71e786f5ccba896db5696d6927b8d1084b25481d

잘못된 도메인 정보 수정하기

erd에서 회원계정 도메인의 이메일에 유니크 키 추가. 게시글,댓글에 유저 계정 필드 추가했다.
회원 id로 로그인 하고 유저를 식별하기 때문에, 당연히 uk여야한다.
이 부분이 설계에 반영되지 않았던 것을 발견하여 수정했다.
테스트는 uk적용으로 기존data.sql의 테스트 데이터와 중복이 발생하므로 userId이름을 수정함

https://github.com/jyc-coder/bulletin-board/pull/34

profile
개발자 꿈나무

0개의 댓글