Spring Boot/STUDY

[Spring Boot] JPA 프로젝트 - 조회수 기능 구현

코맹 2024. 6. 26. 11:45

 

 

list() GET 메서드 주석 처리
// @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";  
  }

 

 

/entity/Board.java 조회수 필드 추가
private Integer hit;  // 24.06.26 조회수 추가

 

 

/service/BoardService.java hitBoard() 메서드 추가
  // 조회수 증가 메서드
  @Transactional    // 조회하면서 업데이트하므로!
  public Board hitBoard(Long bno) {
    // Optional 기능 => Null 체크!
    Optional<Board> oboard = this.boardRepository.findByBno(bno);

    if(oboard.isPresent()) {
      Board board = oboard.get();
      // board.setHit(board.getHit() + 1);   // !!!!! 예외발생
      board.setHit(Optional.ofNullable(board.getHit()).orElse(0) + 1);    // getHit() 값이 null일때 0으로 바꿔라
      
      return board;

    } else {
      throw new NotFoundException("board NOT FOUND!!");
    }

  }

 

💥 null point 에러 발생

board.setHit(board.getHit() + 1);   // !!!!!
  • null값에 +1을 하게 되면 null값을 가지고 오게 되면 null point error 발생!!

 

 

board.setHit(Optional.ofNullable(board.getHit()).orElse(0) + 1);    // getHit() 값이 null일때 0으로 바꿔라
  • getHit() 값이 null일때 0으로 바꿔서 예외처리

 

/controller/BoardController.java detail() 메서드 수정
@GetMapping("/detail/{bno}")
  public String detail(Model model, 
                      @PathVariable("bno") Long bno, ReplyForm replyForm, HttpServletRequest request) {
    
    String prevUrl = request.getHeader("referer");//이전 페이지 변수 담기
    log.info(String.format("현재 이전 페이지: %s", prevUrl));
    // Board board = this.boardService.getBoard(bno);
    Board board = this.boardService.hitBoard(bno);    // 조회수 증가하고 리턴
    
    model.addAttribute("board", board);
    model.addAttribute("prevUrl", prevUrl); //이전 페이지 URL 뷰에 전달

    return "board/detail";
  }
  • 기존의 Board board = this.boardService.getBoard(bno); 주석처리한 것을
  • Board board = this.boardService.hitBoard(bno);로 변경

 

/templates/board/list.html에 new뱃지와 조회수 추가
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
  <div layout:fragment="main-content" class="container my-3">
    <!-- 검색창 영역 -->
    <div class="row my-3 align-items-center">
      <div class="col-8">
        <a th:href="@{|/board/create/${category}|}" 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>
    <!-- 게시글 리스트 -->
    <table class="table table-light table-striped">
      <thead class="table-dark">
        <tr class="text-center">
          <th>번호</th>
          <th style="width: 50%;">제목</th>
          <th>작성자</th>
          <th>조회수</th>
          <th>작성일</th>
        </tr>
      </thead>
      <tbody>
        <tr th:each="board, loop : ${paging}" class="text-center">
          <td th:text="${paging.getTotalElements - (paging.number * paging.size) - loop.index}" class="text-end"></td>
          <td class="text-start">
            <a th:href="@{|/board/detail/${board.bno}|}" th:text="${board.title}"></a>
            <!-- 댓글 개수 띄워주는 함수 -->
            <span th:if="${#lists.size(board.replyList) > 0}" th:text="${#lists.size(board.replyList)}"
               class="badge text-bg-success"></span>
            <span th:if="${#temporals.format(board.createDate, 'yyyy-MM-dd') == #temporals.format(#temporals.createNow(), 'yyyy-MM-dd')}"
               class="badge text-bg-primary">New</span>
          </td>
          <!-- 작성자 추가 -->
          <td>
            <span th:if="${board.writer != null}" th:text="${board.writer.username}"></span>
          </td>
          <!-- 조회수 추가 24.06.26 -->
          <td><span th:text="${board.hit}"></span></td>
          <td th:text="${#temporals.format(board.createDate, 'yyyy-MM-dd')}"></td>
          <!-- <td th:text="${#temporals.format(board.createDate, 'yyyy-MM-dd HH:mm')}"></td> -->
        </tr>
      </tbody>
    </table>
    <!-- 페이징 시작 -->
    <div th:if="${!paging.isEmpty()}">
      <ul class="pagination justify-content-center">

        <!-- 이전 버튼 -->
        <li th:classappend="${!paging.hasPrevious} ? disabled" class="page-item">
          <!-- classappend: 만약에 페이징에 hasPrevious값이 없으면 ? 뒤의 결과를 도출 -->
          <!-- <a th:href="@{|?page=0|}" class="page-link">&lt;&lt;</a> 이전 방식 -->

          <a th:data-page="${0}" href="javascript:void(0)" class="page-link">&lt;&lt;</a>
          <!--  href="javascript:void(0)" : 아무일도 일어나지 않는 형태 -->
        </li>
        <li th:classappend="${!paging.hasPrevious} ? disabled" class="page-item">
          <!-- <a th:href="@{|?page=${paging.number-1}|}" class="page-link">&lt;</a> -->
          <a th:data-page="${paging.number-1}" href="javascript:void(0)" class="page-link">&lt;</a>
        </li>

        <!-- 페이지 번호 버튼 -->
        <li th:each="page : ${#numbers.sequence(0, paging.totalPages-1)}"
            th:if="${page >= paging.number-4 and page <= paging.number+4}"
            th:classappend="${page == paging.number} ? 'active'"
            class="page-item">
          <!-- <a th:href="@{|?page=${page}|}" th:text="${page+1}" class="page-link"></a> -->
          <a th:data-page="${page}" th:text="${page+1}" href="javascript:void(0)" class="page-link"></a>
        </li>

        <!-- 다음 버튼 -->
        <li th:classappend="${!paging.hasNext} ? disabled" class="page-item">
          <!-- <a th:href="@{|?page=${paging.number+1}|}" class="page-link">&gt;</a> -->
          <a th:data-page="${paging.number+1}" href="javascript:void(0)" class="page-link">&gt;</a>
        </li>
        <li th:classappend="${!paging.hasNext} ? disabled" class="page-item">
          <!-- <a th:href="@{|?page=${paging.totalPages-1}|}" class="page-link">&gt;&gt;</a> -->
          <a th:data-page="${paging.totalPages-1}" href="javascript:void(0)" class="page-link">&gt;&gt;</a>
        </li>
      </ul>
    </div>
    <!-- 검색부분 영역 / 이 두 값이 GET URL ?kw=&page=1 계속 가지고 감 -->
    <form th:action="@{|/board/list/${category}|}" method="get" id="searchForm">
      <input type="hidden" id="kw" name="kw" th:value="${kw}">      <!-- id, name 둘다 필요 -->
      <input type="hidden" id="page" name="page" th:value="${paging.number}">
    </form>
  </div>

  <script layout:fragment="sub-script" type="text/javascript">
    const page_elements =document.getElementsByClassName("page-link");
    Array.from(page_elements).forEach(function(element) {
      element.addEventListener("click",function(e) {
        document.getElementById('page').value = this.dataset.page;
        document.getElementById('searchForm').submit();
      });
    });

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

    // 검색창 엔터 검색
    const search_kw = document.getElementById("search_kw");
    search_kw.addEventListener("keypress", function(e) {
      if(event.key == 'Enter') {
       event.preventDefault();  // HTML은 부모자식관계로 구성되므로 자식에서는 이 이벤트가 발생하면 안됨.
       document.getElementById('btn_search').click();
      }
    });
  </script>
</html>

 

 

  • 조회수 추가된 것 확인할 수 있음!