TIL_220806_강의용 게시판 프로젝트 12

창고·2022년 8월 8일
0

들어가기에 앞서
실제 프로젝트 진행했던 코드 내용 및 세부 내용은 일부만 업로드하였습니다

17. 검색 바 기능 구현

(1) 변경 사항

  • UserAccount 도메인 및 JpaRepositoryTest 수정
    (UserId가 UNIQUE 속성이지만 추가가 안되어있었음)
@Table(indexes = {
        @Index(columnList = "userId", unique = true),
        @Index(columnList = "email", unique = true),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy")
})
    @DisplayName("insert 테스트")
    @Test
    void givenTestData_whenInserting_thenWorksFine() {
        // Given
        long previousCount = articleRepository.count(); // 현재 repository의 aritcle 개수 카운트
        // 추가됨
        UserAccount userAccount = userAccountRepository.save(UserAccount.of("newmrcocoball", "pw", null, null, null)); // unique 속성에 따라 중복되지 않은 새 아이디 부여
        Article article = Article.of(userAccount, "new article", "new content", "#spring");

        // When
        // Article savedArticle = articleRepository.save(Article.of("new article", "new content", "#spring"));
        articleRepository.save(article);
        // repository에 새 article 추가

        // Then
        assertThat(articleRepository.count()).isEqualTo(previousCount + 1);

    }

(2) 기능 구현

  • ArticleControllerTest 테스트 추가
    @DisplayName("[VIEW][GET] 게시글 리스트 (게시판 페이지 - 검색어와 함께 호출")
    @Test
    public void givenKeyword_whenRequestingSearchingArticlesView_thenReturnsArticlesView() throws Exception {
        // Given
        SearchType searchType = SearchType.TITLE;
        String searchValue = "title";
        given(articleService.searchArticles(eq(searchType), eq(searchValue), any(Pageable.class))).willReturn(Page.empty());
        given(paginationService.getPaginationBarNumbers(anyInt(), anyInt())).willReturn(List.of(0,1,2,3,4));

        // When & Then
        mvc.perform(
                get("/articles")
                        .queryParam("searchType", "searchType", searchType.name())
                        .queryParam("searchValue", searchValue)
                )
                .andExpect(status().isOk()) // 정상 호출인지
                .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) // HTML 파일의 컨텐츠인지 (호환되는 컨텐츠 포함)
                .andExpect(view().name("articles/index")) // 뷰 이름 검사
                .andExpect(model().attributeExists("articles")) // 내부에 값이 있는지 (이름을 articles로 지정)
                .andExpect(model().attributeExists("searchTypes")); // 내부에 값이 있는지 (이름을 searchTypes로 지정)
        then(articleService).should().searchArticles(eq(searchType), eq(searchValue), any(Pageable.class));
        then(paginationService).should().getPaginationBarNumbers(anyInt(), anyInt());
    }
  • ArticleController 메소드 수정
    @GetMapping
    public String articles(
            @RequestParam(required = false) SearchType searchType,
            @RequestParam(required = false) String searchValue,
            @PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
            ModelMap map
    ) {
        Page<ArticleResponse> articles = articleService.searchArticles(searchType, searchValue, pageable)
                .map(ArticleResponse::from); // dto를 response로 변환하여 페이지로 만듬
        List<Integer> barNumbers = paginationService.getPaginationBarNumbers(pageable.getPageNumber(), articles.getTotalPages());
        // 현재 페이지 번호와 해당 페이지들의 전체 숫자(getTotalPages)를 매개값으로 페이징 내비게이션 바 구축
        map.addAttribute("articles", articles);
        map.addAttribute("paginationBarNumbers", barNumbers);
        map.addAttribute("searchTypes", SearchType.values()); // 추가

        return "articles/index";
    }
  • index.html의 row 부분 변경
     <div class="row">
      <div class="card card-margin search-form">
        <div class="card-body p-0">
          <form action="/articles" method="get"> <!-- 수정 -->
            <div class="row">
              <div class="col-12">
                <div class="row no-gutters">
                  <div class="col-lg-3 col-md-3 col-sm-12 p-0">
                    <label for="search-type" hidden>검색 유형</label>
                    <select class="form-control" id="search-type" name="searchType"> // 수정
                      <option>제목</option>
                      <option>본문</option>
                      <option>id</option>
                      <option>닉네임</option>
                      <option>해시태그</option>
                    </select>
                  </div>
                  <div class="col-lg-8 col-md-6 col-sm-12 p-0">
                    <label for="search-value" hidden>검색어</label>
                    <input type="text" placeholder="검색어..." class="form-control" id="search-value" name="searchValue"> // search-value > search-value
                  </div>
                  <div class="col-lg-1 col-md-3 col-sm-12 p-0">
                    <button type="submit" class="btn btn-base">
                      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search">
                        <circle cx="11" cy="11" r="8"></circle>
                        <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
                      </svg>
                    </button>
                  </div>
                </div>
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  • index.th.xml 수정
<?xml version="1.0"?>
<thlogic>
    <attr sel="#header" th:replace="header :: header" />
    <attr sel="#footer" th:replace="footer :: footer" />

    <attr sel="main" th:object="${articles}">
        <attr sel="#search-type" th:remove="all-but-first">
            <attr sel="option[0]"
                  th:each="searchType : ${searchTypes}"
                  th:value="${searchType.name}"
                  th:text="${searchType.description}"
                  th:selected="${param.searchType != null && (param.searchType.toString == searchType.name)}"
            />
        </attr>
        <attr sel="#search-value" th:value="${param.searchValue}" />

        <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' : '') : ''),
            searchType=${param.searchType},
            searchValue=${param.searchValue}
        )}"/>
                <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' : '') : ''),
            searchType=${param.searchType},
            searchValue=${param.searchValue}
        )}"/>
                <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' : '') : ''),
            searchType=${param.searchType},
            searchValue=${param.searchValue}
        )}"/>
                <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' : '') : ''),
            searchType=${param.searchType},
            searchValue=${param.searchValue}
        )}"/>
            </attr>

            <attr sel="tbody" th:remove="all-but-first">
                <attr sel="tr[0]" th:each="article : ${articles}">
                    <attr sel="td.title/a" th:text="${article.title}" th:href="@{'/articles/' + ${article.id}}" />
                    <attr sel="td.hashtag" th:text="${article.hashtag}" />
                    <attr sel="td.user-id" th:text="${article.nickname}" />
                    <attr sel="td.created-at/time" th:datetime="${article.createdAt}" th:text="${#temporals.format(article.createdAt, 'yyyy-MM-dd')}" />
                </attr>
            </attr>
        </attr>

        <attr sel="#pagination">
            <attr sel="li[0]/a"
                  th:text="'previous'"
                  th:href="@{/articles(page=${articles.number - 1}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
                  th:class="'page-link' + (${articles.number} <= 0 ? ' disabled' : '')"
            />
            <attr sel="li[1]" th:class="page-item" th:each="pageNumber : ${paginationBarNumbers}">
                <attr sel="a"
                      th:text="${pageNumber + 1}"
                      th:href="@{/articles(page=${pageNumber}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
                      th:class="'page-link' + (${pageNumber} == ${articles.number} ? ' disabled' : '')"
                />
            </attr>
            <attr sel="li[2]/a"
                  th:text="'next'"
                  th:href="@{/articles(page=${articles.number + 1}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
                  th:class="'page-link' + (${articles.number} >= ${articles.totalPages - 1} ? ' disabled' : '')"
            />
        </attr>
    </attr>
</thlogic>
  • index.th.xml의 해당 코드
    • 직전 내용과 현재 검색 내용의 검색 항목이 같을 경우 항목이 초기화되지 않고 연속적으로 검색할 수 있게 만듬 ('ID' 검색 상태에서 'ID' 검색을 할 경우 'ID' 항목이 초기화되지 않음)
    • 검색 시의 parameter에서 가져온 searchType을 문자화한 것이 searchType과 동일할 경우 작동
th:selected="${param.searchType != null && (param.searchType.toString == searchType.name)}
  • SearchType 주석을 위해 SearchType 수정
     TITLE("제목"),
    CONTENT("본문"),
    ID("유저 ID"),
    NICKNAME("닉네임"),
    HASHTAG("해시태그");

    @Getter private final String description;

    SearchType(String description) {
        this.description = description;
    }

18. 해시태그 검색 페이지 구현

(1) 서비스, 컨트롤러 생성 및 테스트 작성

  • 제목, 본문, 이름 검색 페이지는 별도로 구현하지 않고 (위에서 구현한 기능과 중복) 해시태그 검색 페이지를 별도로 만들어보도록 한다
  • ArticleServiceTest 메소드 추가 (검색어 없이 해시태그 검색 시)
    @DisplayName("READ - 검색어 없이 게시글을 해시태그 검색하면, 빈 페이지를 반환")
    @Test
    void givenNoSearchParameters_whenSearchingArticlesViaHashtag_thenReturnsEmptyPage() {
        // Given
        Pageable pageable = Pageable.ofSize(20);

        // When
        Page<ArticleDto> articles = sut.searchArticlesViaHashtag(null, pageable);

        // Then
        // assertThat(articles).isEmpty(); 아래와 동일
        assertThat(articles).isEqualTo(Page.empty(pageable));
        then(articleRepository).shouldHaveNoInteractions();
    }
  • ArticleService 메소드 임시 추가
  • ArticleServiceTest 메소드 추가 (해시태그 검색 시 게시글 반환)
    @DisplayName("READ - 게시글을 해시태그 검색하면, 게시글 페이지를 반환")
    @Test
    void givenHashtag_whenSearchingArticlesViaHashtag_thenReturnsArticlePage() {
        // Given
        String hashtag = "#java";
        Pageable pageable = Pageable.ofSize(20);
        given(articleRepository.findByHashtag(hashtag, pageable)).willReturn(Page.empty(pageable));

        // When
        Page<ArticleDto> articles = sut.searchArticlesViaHashtag(hashtag, pageable);

        // Then
        // assertThat(articles).isEmpty(); 아래와 동일
        assertThat(articles).isEqualTo(Page.empty(pageable));
        then(articleRepository).should().findByHashtag(hashtag, pageable);
    }
  • ArticleServiceTest 메소드 추가 (해시태그 조회 시 리스트 반환)
    @DisplayName("READ - 해시태그를 조회하면, 고유 해시태그 리스트를 반환한다")
    @Test
    void givenNothing_whenCalling_thenReturnsHashtags() {
        // Given
        List<String> expectedHashtags = List.of("#java", "#spring", "python");
        given(articleRepository.findAllDistinctHashtags()).willReturn(expectedHashtags);

        // When
        List<String> actualHashtags = sut.getHashtags();

        // Then
        assertThat(actualHashtags).isEqualTo(expectedHashtags);
        then(articleRepository).should().findAllDistinctHashtags();
    }
  • ArticleRepository 메소드 추가 및 ArticleRepositoryCustom 인터페이스 추가
    • Querydsl 필요, repository package 내부에 querydsl package,
      ArticleRepositoryCustom Interface 생성
public interface ArticleRepositoryCustom {

    List<String> findAllDistinctHashtags();
}
  • ArticleRepositoryCustom의 메소드를 구현할 클래스 ArticleRepositoryCumstomImpl 생성
    (Impl은 네이밍 시 필수)
import com.fastcampus.projectboard.domain.Article;
import com.fastcampus.projectboard.domain.QArticle;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;

import java.util.List;

public class ArticleRepositoryCustomImpl extends QuerydslRepositorySupport implements ArticleRepositoryCustom {

    public ArticleRepositoryCustomImpl() {
        super(Article.class);
    }

    @Override
    public List<String> findAllDistinctHashtags() {
        QArticle article = QArticle.article;

        return from(article)
                .distinct()
                .select(article.hashtag)
                .where(article.hashtag.isNotNull())
                .fetch();
        // JPQLQuery<Article> 에서 hashtag가 null이 아닌 것들 중에서 고유의 hashtag를 가져옴
   }
}
  • ArticleRepository가 ArticleRepositoryCustom 상속
public interface ArticleRepository extends
        JpaRepository<Article, Long>,
        ArticleRepositoryCustom, // 추가
        QuerydslPredicateExecutor<Article>, // Article 안의 모든 필드에 대한 기본 검색 기능 추가
        QuerydslBinderCustomizer<QArticle> {
  • ArticleService 메소드 임시 추가

(2) 테스트 기반 기능 구현

  • 해시태그 리스트 조회 기능 구현
    public List<String> getHashtags() {
        return articleRepository.findAllDistinctHashtags();
    }
  • 해시태그 검색 기능 구현
    @Transactional(readOnly = true)
    public Page<ArticleDto> searchArticlesViaHashtag(String hashtag, Pageable pageable) {

        if (hashtag == null || hashtag.isBlank()) {
            return Page.empty(pageable);
        }
        return articleRepository.findByHashtag(hashtag, pageable).map(ArticleDto::from);
    }
  • ArticleController 핸들러 메소드 추가
    @GetMapping("/search-hashtag")
    public String searchHashtag(
            @RequestParam(required = false) String searchValue,
            @PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
            ModelMap map
    ) {
        return "articles/search-hashtag";
    }
  • ArticleControllerTest 해시태그 검색 뷰 테스트 (정상 호출)
   @DisplayName("[VIEW][GET] 게시글 해시태그 검색 페이지 - 정상 호출")
    @Test
    public void givenNothing_whenRequestingArticleSearchingHashtagView_thenReturnsArticleSearchHashtagSearchView() throws Exception {
        // Given
        List<String> hashtags = List.of("#Java", "#Spring", "#Python");
        given(articleService.searchArticlesViaHashtag(eq(null), any(Pageable.class))).willReturn(Page.empty());
        given(articleService.getHashtags()).willReturn(hashtags);
        given(paginationService.getPaginationBarNumbers(anyInt(), anyInt())).willReturn(List.of(1, 2, 3, 4, 5));

        // When & Then
        mvc.perform(get("/articles/search-hashtag"))
                .andExpect(status().isOk()) // 정상 호출인지
                .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) // HTML 파일의 컨텐츠인지 (호환되는 컨텐츠 포함)
                .andExpect(view().name("articles/search-hashtag")) // 뷰 이름 검사
                .andExpect(model().attribute("articles", Page.empty()))
                .andExpect(model().attribute("hashtags", hashtags))
                .andExpect(model().attributeExists("paginationBarNumbers"))
                .andExpect(model().attribute("searchType", SearchType.HASHTAG));
        then(articleService).should().searchArticlesViaHashtag(eq(null), any(Pageable.class));
        then(articleService).should().getHashtags();
        then(paginationService).should().getPaginationBarNumbers(anyInt(), anyInt());
    }
  • ArticleControllerTest 해시태그 검색 뷰 테스트 (정상 호출, 해시태그 입력)
    @DisplayName("[VIEW][GET] 게시글 해시태그 검색 페이지 - 정상 호출, 해시태그 입력")
    @Test
    public void givenHashtag_whenRequestingArticleSearchingHashtagView_thenReturnsArticleSearchHashtagSearchView() throws Exception {
        // Given
        String hashtag = "#java";
        List<String> hashtags = List.of("#Java", "#Spring", "#Python");
        given(articleService.searchArticlesViaHashtag(eq(hashtag), any(Pageable.class))).willReturn(Page.empty());
        given(articleService.getHashtags()).willReturn(hashtags);
        given(paginationService.getPaginationBarNumbers(anyInt(), anyInt())).willReturn(List.of(1, 2, 3, 4, 5));

        // When & Then
        mvc.perform(get("/articles/search-hashtag")
                        .queryParam("searchValue", hashtag)
                )
                .andExpect(status().isOk()) // 정상 호출인지
                .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) // HTML 파일의 컨텐츠인지 (호환되는 컨텐츠 포함)
                .andExpect(view().name("articles/search-hashtag")) // 뷰 이름 검사
                .andExpect(model().attribute("articles", Page.empty()))
                .andExpect(model().attribute("hashtags", hashtags))
                .andExpect(model().attributeExists("paginationBarNumbers"))
                .andExpect(model().attribute("searchType", SearchType.HASHTAG));
        then(articleService).should().searchArticlesViaHashtag(eq(hashtag), any(Pageable.class));
        then(articleService).should().getHashtags();
        then(paginationService).should().getPaginationBarNumbers(anyInt(), anyInt());
    }
  • ArticleController 핸들러 메소드 수정
    @GetMapping("/search-hashtag")
    public String searchHashtag(
            @RequestParam(required = false) String searchValue,
            @PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
            ModelMap map
    ) {
        Page<ArticleResponse> articles = articleService.searchArticlesViaHashtag(searchValue, pageable)
                .map(ArticleResponse::from); // dto를 response로 변환하여 페이지로 만듬
        List<Integer> barNumbers = paginationService.getPaginationBarNumbers(pageable.getPageNumber(), articles.getTotalPages());
        // 현재 페이지 번호와 해당 페이지들의 전체 숫자(getTotalPages)를 매개값으로 페이징 내비게이션 바 구축
        List<String> hashtags = articleService.getHashtags();

        map.addAttribute("articles", articles);
        map.addAttribute("hashtags", hashtags);
        map.addAttribute("paginationBarNumbers", barNumbers);
        map.addAttribute("searchType", SearchType.HASHTAG);;
        
        return "articles/search-hashtag";
    }

(3) 해시태그 페이지 뷰 구현

  • search-hashtag.html
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="">
  <meta name="author" content="Uno Kim">
  <title>해시태그 검색</title>

  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
  <link href="/css/articles/table-header.css" rel="stylesheet">
</head>

<body>
<header id="header">
  헤더 삽입부
  <hr>
</header>

<main class="container">
  <header class="py-5 text-center">
    <h1>Hashtags</h1>
  </header>

  <section class="row">
    <div id="hashtags" class="col-9 d-flex flex-wrap justify-content-evenly">
      <div class="p-2">
        <h2 class="text-center lh-lg font-monospace"><a href="#">#java</a></h2>
      </div>
    </div>
  </section>

  <hr>

  <table class="table" id="article-table">
    <thead>
    <tr>
      <th class="title col-6"><a>제목</a></th>
      <th class="content col-4"><a>본문</a></th>
      <th class="user-id"><a>작성자</a></th>
      <th class="created-at"><a>작성일</a></th>
    </tr>
    </thead>
    <tbody>
    <tr>
      <td class="title"><a>첫글</a></td>
      <td class="content"><span class="d-inline-block text-truncate" style="max-width: 300px;">본문</span></td>
      <td class="user-id">Uno</td>
      <td class="created-at"><time>2022-01-01</time></td>
    </tr>
    <tr>
      <td>두번째글</td>
      <td>본문</td>
      <td>Uno</td>
      <td><time>2022-01-02</time></td>
    </tr>
    <tr>
      <td>세번째글</td>
      <td>본문</td>
      <td>Uno</td>
      <td><time>2022-01-03</time></td>
    </tr>
    </tbody>
  </table>

  <nav id="pagination" aria-label="Page navigation">
    <ul class="pagination justify-content-center">
      <li class="page-item"><a class="page-link" href="#">Previous</a></li>
      <li class="page-item"><a class="page-link" href="#">1</a></li>
      <li class="page-item"><a class="page-link" href="#">Next</a></li>
    </ul>
  </nav>

</main>

<footer id="footer">
  <hr>
  푸터 삽입부
</footer>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2" crossorigin="anonymous"></script>
</body>
</html>
  • search-hashtag.th.xml
<?xml version="1.0"?>
<thlogic>
    <attr sel="#header" th:replace="header :: header" />
    <attr sel="#footer" th:replace="footer :: footer" />

    <attr sel="main" th:object="${articles}">
        <attr sel="#hashtags" th:remove="all-but-first">
            <attr sel="div" th:each="hashtag : ${hashtags}">
                <attr sel="a" th:class="'text-reset'" th:text="${hashtag}" th:href="@{/articles/search-hashtag(
            page=${param.page},
            sort=${param.sort},
            searchType=${searchType.name},
            searchValue=${hashtag}
        )}" />
            </attr>
        </attr>

        <attr sel="#article-table">
            <attr sel="thead/tr">
                <attr sel="th.title/a" th:text="'제목'" th:href="@{/articles/search-hashtag(
            page=${articles.number},
            sort='title' + (*{sort.getOrderFor('title')} != null ? (*{sort.getOrderFor('title').direction.name} != 'DESC' ? ',desc' : '') : ''),
            searchType=${searchType.name},
            searchValue=${param.searchValue}
        )}"/>
                <attr sel="th.content/a" th:text="'본문'" th:href="@{/articles/search-hashtag(
            page=${articles.number},
            sort='content' + (*{sort.getOrderFor('content')} != null ? (*{sort.getOrderFor('content').direction.name} != 'DESC' ? ',desc' : '') : ''),
            searchType=${searchType.name},
            searchValue=${param.searchValue}
        )}"/>
                <attr sel="th.user-id/a" th:text="'작성자'" th:href="@{/articles/search-hashtag(
            page=${articles.number},
            sort='userAccount.userId' + (*{sort.getOrderFor('userAccount.userId')} != null ? (*{sort.getOrderFor('userAccount.userId').direction.name} != 'DESC' ? ',desc' : '') : ''),
            searchType=${searchType.name},
            searchValue=${param.searchValue}
        )}"/>
                <attr sel="th.created-at/a" th:text="'작성일'" th:href="@{/articles/search-hashtag(
            page=${articles.number},
            sort='createdAt' + (*{sort.getOrderFor('createdAt')} != null ? (*{sort.getOrderFor('createdAt').direction.name} != 'DESC' ? ',desc' : '') : ''),
            searchType=${searchType.name},
            searchValue=${param.searchValue}
        )}"/>
            </attr>
            <attr sel="tbody" th:remove="all-but-first">
                <attr sel="tr[0]" th:each="article : ${articles}">
                    <attr sel="td.title/a" th:text="${article.title}" th:href="@{'/articles/' + ${article.id}}" />
                    <attr sel="td.content/span" th:text="${article.content}" />
                    <attr sel="td.user-id" th:text="${article.nickname}" />
                    <attr sel="td.created-at/time" th:datetime="${article.createdAt}" th:text="${#temporals.format(article.createdAt, 'yyyy-MM-dd')}" />
                </attr>
            </attr>
        </attr>

        <attr sel="#pagination">
            <attr sel="ul">
                <attr sel="li[0]/a"
                      th:text="'previous'"
                      th:href="@{/articles(page=${articles.number - 1}, searchType=${searchType.name}, searchValue=${param.searchValue})}"
                      th:class="'page-link' + (${articles.number} <= 0 ? ' disabled' : '')"
                />
                <attr sel="li[1]" th:class="page-item" th:each="pageNumber : ${paginationBarNumbers}">
                    <attr sel="a"
                          th:text="${pageNumber + 1}"
                          th:href="@{/articles(page=${pageNumber}, searchType=${searchType.name}, searchValue=${param.searchValue})}"
                          th:class="'page-link' + (${pageNumber} == ${articles.number} ? ' disabled' : '')"
                    />
                </attr>
                <attr sel="li[2]/a"
                      th:text="'next'"
                      th:href="@{/articles(page=${articles.number + 1}, searchType=${searchType.name}, searchValue=${param.searchValue})}"
                      th:class="'page-link' + (${articles.number} >= ${articles.totalPages - 1} ? ' disabled' : '')"
                />
            </attr>
        </attr>
    </attr>
</thlogic>

profile
공부했던 내용들을 모아둔 창고입니다.

0개의 댓글