Spring Boot/STUDY

[Spring Boot] JPA 프로젝트 - Specification 인터페이스를 사용한 검색 기능 구현(1)

코맹 2024. 6. 24. 17:22

 

 

/service/BoardService.java search() 메서드 추가
import org.springframework.data.jpa.domain.specification;   // 복합쿼리 생성용
  public Specification<Board> searchBoard(String keyword) {
    return new Specification<Board>() {
      private static final long serialVersionUID = 1L;  // 필요한 값이라서 추가

      @Override
      @Nullable
      public Predicate toPredicate(Root<Board> b, CriteriaQuery<?> query, CriteriaBuilder cb) {
        // query를 JPA로 생성
        query.distinct(true); // 중복 제거
        Join<Board, Reply> r = b.join("replyList", JoinType.LEFT);

        return cb.or(cb.like(b.get("title"), "%" + keyword + "%"),    // 게시글 제목에서 검색
                     cb.like(b.get("content"), "%" + keyword + "%"),   // 게시글 내용에서 검색
                     cb.like(r.get("content"), "%" + keyword + "%") // 댓글 내용에서 검색
                     );
      }
    };
  }

 

더보기

SELECT DISTINCT

b.BNO

, b.TITLE

, b.CONTENT

, b.CREATE_DATE

, b.MODIFY_DATE

, b.WRITER_MID

FROM BOARD b

LEFT OUTER JOIN REPLY r ON b.BNO = r.BOARD_BNO

WHERE b.title LIKE '%사람%'

OR b.content LIKE '%사람%'

OR r.content LIKE '%사람%';

  • 위와 같은 쿼리문을 jpa가 대신 해줌

 

/repository/BoardRepository.java findAll() 메서드 추가
Page<Board> findAll(Specification<Board> spec, Pageable pageable);

 

 

/service/BoardService.java getList() 메서드 추가 생성
public Page<Board> getList(int page, String keyword) {
    List<Sort.Order> sorts = new ArrayList<>();
    sorts.add(Sort.Order.desc("createDate"));
    Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));   // pageSize를 동적으로도 변경할 수 있음. 나중에...
    
    Specification<Board> spec = searchBoard(keyword);
    return this.boardRepository.findAll(spec, pageable);

  }

 

 

/controller/BoardController.java list() 메서드 추가
  @GetMapping("/list")
  public String list(Model model, @RequestParam(value="page", defaultValue = "0") int page,
                     @RequestParam(value = "kw", defaultValue = "") String keyword) {
    Page<Board> paging = this.boardService.getList(page, keyword);  // 검색 추가
    model.addAttribute("paging", paging);
    model.addAttribute("kw", keyword);

    return "board/list";  
  }

🚫 기존의 @GetMapping("/list") 메서드는 주석처리한 후 작성해야 함! (주석처리 안하면 동일하기 때문에 오류 발생)

 

 

/templates/board/list.html 검색창 추가, searchForm 폼 영역 추가
  • 검색창 추가
<div class="row my-3 align-items-center">
    <!-- 등록 버튼 -->
    <div class="col-8">
      <a th:href="@{/board/create}" class="btn btn-sm btn-primary my-2">게시글 등록</a>
    </div>
    <!-- 검색창 영역 -->
    <div class="col-4">
      <div class="input-group">
        <input type="text" id="search_kw" class="form-control" th:value="${kw}">
        <button id="btn_search" type="button" class="btn btn-sm btn-outline-secondary">찾기</button>
      </div>
    </div>
</div>

 

  • searchForm 폼 영역 추가
<form th:action="@{}" method="get" id="searchForm">
    <input type="hidden" id="kw" name="kw" th:value="${kw}">
    <input type="hidden" id="page" name="page" th:value="${paging.number}">
</form>

 

  • 페이징 부분 <a> 태그 수정
<!-- 페이징 시작 -->
  <div th:if="${!paging.isEmpty()}">
    <ul class="pagination justify-content-center">
      <!-- 이전버튼 -->
      <li th:classappend="${!paging.hasPrevious} ? disabled" class="page-item">
        <a th:data-page="${0}" class="page-link" href="javascript:void(0)">《</a>
      </li>
      <li th:classappend="${!paging.hasPrevious} ? disabled" class="page-item">
        <a th:data-page="${paging.number-1}" class="page-link" href="javascript:void(0)">〈</a>
      </li>
      <!-- 페이지번호버튼 -->
      <li th:each="page : ${#numbers.sequence(0, paging.totalPages-1)}"
        th:if="${page >= paging.number-5 and page <= paging.number+5}"
        th:classappend="${page == paging.number} ? active" class="page-item">
        <a th:data-page="${page}" th:text="${page+1}" class="page-link" href="javascript:void(0)">〈</a>
      </li>
      <!-- 다음버튼 -->
      <li th:classappend="${!paging.hasNext} ? disabled" class="page-item">
        <a th:data-page="${paging.number+1}" class="page-link" href="javascript:void(0)">〈</a>
      </li>
      <li th:classappend="${!paging.hasNext} ? disabled" class="page-item">
        <a th:data-page="${paging.totalPages-1}" class="page-link" href="javascript:void(0)">《</a>
      </li>
    </ul>
  </div>
  <!-- 페이징 끝 -->

 

 

  • 가장 아래에 script 추가하기
<script layout:fragment="sub-script" type="text/javascript">
  const page_elements = document.getElementsByClassName("page-link");
  Array.from(page_elements).forEach(function(elements) {
    elements.addEventListener('click', function(){
      document.getElementById('page').value = this.dataset.page;
      document.getElementById('searchForm').submit(); // 서브밋 발동

    });
  });

  const btn_search = document.getElementById("btn_search");
  btn_search.addEventListener('click', function() {
    document.getElementById('kw').value = document.getElementById('search_kw').value;
    document.getElementById('page').value = 0;  // 검색할 경우 0 페이지부터
    document.getElementById('searchForm').submit(); // 서브밋 발동

  });

  // 검색창을 검색하고 엔터치면 검색버튼 클릭 발생
  var search_kw = document.getElementById('search_kw');
  search_kw.addEventListener('keypress', function(event) {
    if(event.key == 'Enter') {
      event.preventDefault();   // html은 부모, 자식 관계로 구성되어 있으므로 자식에서는 해당 이벤트가 발생하면 안됨. 이를 막기 위한 preventDefault();
      document.getElementById('btn_search').click();

    }
  });
</script>

 

 

  • 검색하면 검색 문자열이 포함된 게시물 리스트가 뜨는 걸 확인할 수 있음