[스프링부트] 20. Blog v2 : Board

문정준's avatar
Apr 07, 2025
[스프링부트] 20. Blog v2 : Board
✏️

블로그 내 기능들은 로그인 한 유저만 사용 가능해야 함

  • 세션 정보를 받아 정보가 일치하면 기능 이용 가능하도록 부가 로직 추가

1. 글 쓰기

  • 글을 작성했을 때 Relation Mapping을 통해 쿼리 없이 자동으로 DB에 데이터가 들어가도록 설정
 
  • BoardController
    • 글을 작성하고 글쓰기완료 버튼을 누르면 session 정보와 DTO를 전송
@GetMapping("/board/save-form") public String saveForm(HttpSession session) { User validatedUser = (User) session.getAttribute("validatedUser"); if (validatedUser == null) throw new RuntimeException("인증이 필요합니다."); return "board/save-form"; } @PostMapping("/board/save") public String save(BoardRequest.SaveDTO saveDTO, HttpSession session) { User validatedUser = (User) session.getAttribute("validatedUser"); if (validatedUser == null) throw new RuntimeException("인증이 필요합니다."); boardService.글쓰기(saveDTO, validatedUser); return "redirect:/"; }
 
  • BoardRequest
    • user 정보를 세션에서 받아옴
@Data public static class SaveDTO { private String title; private String content; private String isPublic; public Board toEntity(User user) { return Board.builder() .title(title) .content(content) .isPublic(isPublic == null ? false : true) .user(user) .build(); } }
 
  • BoardService
@Transactional public void 글쓰기(BoardRequest.SaveDTO saveDTO, User user) { Board board = saveDTO.toEntity(user); boardRepository.write(board); }
 
  • BoardRepository
public void write(Board board) { em.persist(board); }
 
  • board/save-form.mustache
{{> layout/header}} <div class="container p-5"> <div class="card"> <div class="card-header"><b>글쓰기 화면입니다</b></div> <div class="card-body"> <form action="/board/save" method="post"> <div class="mb-3"> <input type="text" class="form-control" placeholder="Enter title" name="title"> </div> <div class="mb-3"> <textarea class="form-control" rows="5" name="content"></textarea> </div> <!-- ✅ 공개 여부 체크박스 --> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" name="isPublic"> <label class="form-check-label" for="isPublic"> 공개 글로 작성하기 </label> </div> <button class="btn btn-primary form-control">글쓰기완료</button> </form> </div> </div> </div> {{> layout/footer}}
 

결과

  • 글을 작성하면 목록 갱신
notion image
 
notion image

2. 글 목록

  • BoardController
    • 작성된 글 목록을 List로 받아 폼에 출력
    • 비공개 글은 다른 사람에게 보이면 안되고, 내가 쓴 글은 비공개여도 보여야 함
      • 세션 정보를 받아 userId의 여부에 따라 다른 글 목록 표시
@GetMapping("/") public String home(HttpServletRequest request, HttpSession session) { User validatedUser = (User) session.getAttribute("validatedUser"); if (validatedUser == null) { request.setAttribute("models", boardService.글목록보기(null)); } else { request.setAttribute("models", boardService.글목록보기(validatedUser.getId())); } return "board/list"; }
 
  • BoardService
public List<Board> 글목록보기(Integer userId) { return boardRepository.findAll(userId); }
 
  • BoardRepository
    • userId가 null인지 여부에 따라 2가지의 sql 작동
    • userId가 null이 아니면 (로그인했으면) 자신의 글을 보여주어야 함
      • where b.isPublic = true or b.user.id = :userId
public List<Board> findAll(Integer userId) { String s1 = "select b from Board b where b.isPublic = true or b.user.id = :userId order by b.id desc"; String s2 = "select b from Board b where b.isPublic = true order by b.id desc"; Query q = em.createQuery(userId == null ? s2 : s1, Board.class); if(userId != null) q.setParameter("userId", userId); return q.getResultList(); }
 
  • board/list.mustache
{{> layout/header}} <div class="container p-5"> <div class="mb-3 d-flex justify-content-end"> <form class="d-flex"> <input class="form-control me-2" type="text" placeholder="검색..."> <button class="btn btn-primary" type="button">Search</button> </form> </div> {{#models}} <div class="card mb-3"> <div class="card-body"> <div class="d-flex justify-content-between"> <h4 class="card-title mb-3">{{title}}</h4> {{^isPublic}} <div class="d-flex align-items-start"> <span class="badge bg-secondary">비공개</span> </div> {{/isPublic}} </div> <a href="/board/{{id}}" class="btn btn-primary">상세보기</a> </div> </div> {{/models}} <ul class="pagination d-flex justify-content-center"> <li class="page-item disabled"><a class="page-link" href="#">Previous</a></li> <li class="page-item"><a class="page-link" href="#">Next</a></li> </ul> </div> {{> layout/footer}}
 

Test

  • BoardRepositoryTest
@Autowired // DI private BoardRepository boardRepository; @Test public void findAll_test() { // given // when List<Board> boardList = boardRepository.findAll(); // Lazy -> Board -> User(id=1) // Eager -> N+1 -> Board조회 -> 연관된 User 유저 수 만큼 주회 // Eager -> Join -> 한방쿼리 System.out.println("--------------------"); boardList.get(0).getUser().getUsername(); System.out.println("--------------------"); // eye }
  • findAll()의 동작을 확인하기 위한 test
    • user의 select 쿼리가 3번 발동함
Hibernate: select b1_0.id, b1_0.content, b1_0.created_at, b1_0.is_public, b1_0.title, b1_0.user_id from board_tb b1_0 Hibernate: select u1_0.id, u1_0.created_at, u1_0.email, u1_0.password, u1_0.username from user_tb u1_0 where u1_0.id=? Hibernate: select u1_0.id, u1_0.created_at, u1_0.email, u1_0.password, u1_0.username from user_tb u1_0 where u1_0.id=? Hibernate: select u1_0.id, u1_0.created_at, u1_0.email, u1_0.password, u1_0.username from user_tb u1_0 where u1_0.id=?
 
  • 글을 작성한 user = 3명
    • 4번이 아닌 이유? PC에 캐싱
insert into board_tb(title, content, user_id, is_public, created_at) values ('제목1', '내용1', 1, true, now()); insert into board_tb(title, content, user_id, is_public, created_at) values ('제목2', '내용2', 1, true, now()); insert into board_tb(title, content, user_id, is_public, created_at) values ('제목3', '내용3', 2, true, now()); insert into board_tb(title, content, user_id, is_public, created_at) values ('제목4', '내용4', 3, true, now());
 
  • user가 1명일 경우
Hibernate: select b1_0.id, b1_0.content, b1_0.created_at, b1_0.is_public, b1_0.title, b1_0.user_id from board_tb b1_0 Hibernate: select u1_0.id, u1_0.created_at, u1_0.email, u1_0.password, u1_0.username from user_tb u1_0 where u1_0.id=?
 
  • FetchType = LAZY
Hibernate: select b1_0.id, b1_0.content, b1_0.created_at, b1_0.is_public, b1_0.title, b1_0.user_id from board_tb b1_0
Hibernate: select b1_0.id, b1_0.content, b1_0.created_at, b1_0.is_public, b1_0.title, b1_0.user_id from board_tb b1_0 -------------------- Hibernate: select u1_0.id, u1_0.created_at, u1_0.email, u1_0.password, u1_0.username from user_tb u1_0 where u1_0.id=? --------------------
 
✏️
Lazy vs Eager
Lazy 전략 : 릴레이션 매핑을 사용하지 않음
Eager 전략 : 무조건 릴레이션 매핑을 사용함
Eager의 경우 불필요한 데이터를 가져올 수 있기 때문에, Lazy 전략을 사용하되 릴레이션 매핑이 필요하면 직접 쿼리를 작성
 

결과

  • 로그인 안 했을 때
notion image
  • ssar로 로그인 했을 때 (비공개 글)
notion image
 

3. 글 상세보기

  • BoardController
    • 글을 작성하고 글쓰기완료 버튼을 누르면 session 정보와 DTO를 전송
@GetMapping("/board/{id}") public String detailForm(@PathVariable("id") int id, HttpSession session, HttpServletRequest request) { Integer userId = null; User validatedUser = (User) session.getAttribute("validatedUser"); if(validatedUser != null) userId = validatedUser.getId(); BoardResponse.DetailDTO board = boardService.글상세보기(id, userId); request.setAttribute("model", board); return "board/detail"; }
 
  • BoardResponse
    • 글쓴이와 로그인한 유저가 일치해야 수정, 삭제 표시가 가능
    • isOwner 속성 추가
@Data public static class DetailDTO { private Integer id; private String title; private String content; private Boolean isPublic; private Boolean isOwner; private Integer userId; private String username; private Timestamp createdAt; public DetailDTO(Board board, Integer sessionUserId) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.isPublic = board.getIsPublic(); this.isOwner = board.getUser().getId() == sessionUserId; this.userId = board.getUser().getId(); this.username = board.getUser().getUsername(); this.createdAt = board.getCreatedAt(); } }
 
  • BoardService
public BoardResponse.DetailDTO 글상세보기(int id, Integer sessionUserId) { BoardResponse.DetailDTO detailDTO = new BoardResponse.DetailDTO(boardRepository.findByIdJoinUser(id), sessionUserId); return detailDTO; }
 
  • BoardRepository
public Board findByIdJoinUser(int id) { // inner join은 on 절 생략 가능 Query q = em.createQuery("select b from Board b join fetch b.user where b.id = :id", Board.class); q.setParameter("id", id); return (Board) q.getSingleResult(); }
 
  • board/detail.mustache
{{> layout/header}} <div class="container p-5"> {{#model.isOwner}} <!-- 수정삭제버튼 --> <div class="d-flex justify-content-end"> <a href="/board/{{model.id}}/update-form" class="btn btn-warning me-1">수정</a> <form action="/board/{{model.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/model.isOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{model.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{model.content}} </div> </div> <!-- AJAX 좋아요 영역 --> <div class="my-3 d-flex align-items-center"> <span class="my-like-heart" onclick="likeToggle()">&#10084;</span> <span><b id="likeCount">12</b>명이 이 글을 좋아합니다</span> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/reply/save" method="post"> <textarea class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">cos</div> <div>댓글 내용입니다</div> </div> <form action="/reply/1/delete" method="post"> <button class="btn">🗑</button> </form> </div> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">ssar</div> <div>댓글 내용입니다</div> </div> <form action="/reply/1/delete" method="post"> <button class="btn">🗑</button> </form> </div> </div> </div> </div> <script> function likeToggle() { let likeHeart = document.querySelector('.my-like-heart'); console.log(likeHeart); likeHeart.classList.toggle('liked'); } </script> {{> layout/footer}}

Test

  • BoardRepositoryTest
@Test public void findByIdJoinUser_test() { // given int id = 1; // when Board board = boardRepository.findByIdJoinUser(id); // eye System.out.println(board); }
 
  • join fetch
Hibernate: select b1_0.id, b1_0.content, b1_0.created_at, b1_0.is_public, b1_0.title, u1_0.id, u1_0.created_at, u1_0.email, u1_0.password, u1_0.username from board_tb b1_0 join user_tb u1_0 on u1_0.id=b1_0.user_id where b1_0.id=? Board{id=1, title='제목1', content='내용1', isPublic=true, user=User{id=1, username='ssar', password='1234', email='ssar@nate.com', createdAt=2025-04-07 14:42:21.914297}, createdAt=2025-04-07 14:42:21.91629}
 
  • join
    • select 2회 실행
Hibernate: select b1_0.id, b1_0.content, b1_0.created_at, b1_0.is_public, b1_0.title, b1_0.user_id from board_tb b1_0 join user_tb u1_0 on u1_0.id=b1_0.user_id where b1_0.id=? Hibernate: select u1_0.id, u1_0.created_at, u1_0.email, u1_0.password, u1_0.username from user_tb u1_0 where u1_0.id=? Board{id=1, title='제목1', content='내용1', isPublic=true, user=User{id=1, username='ssar', password='1234', email='ssar@nate.com', createdAt=2025-04-07 14:44:58.553253}, createdAt=2025-04-07 14:44:58.555246}
 
✏️
join vs join fetch
join : select가 2회 실행 (FetchType = LAZY)
join fetch : join된 쿼리문이 1회만 실행
 

결과

  • 로그인 안 했을 때
    • 수정, 삭제 버튼이 보이지 않음
notion image
  • 로그인 했을 때 1
    • 자신의 글은 수정, 삭제 버튼 확인 가능
notion image
  • 로그인 했을 때 2
    • 다른 사람의 글은 수정, 삭제 버튼 확인 불가
notion image

4. 글 삭제

  • BoardController
@PostMapping("/board/{id}/delete") public String delete(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증이 필요합니다"); boardService.글삭제(id); return "redirect:/"; }
 
  • BoardService
@PostMapping("/board/{id}/delete") public String delete(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.글삭제(id, sessionUser.getId()); return "redirect:/"; }
 
  • BoardRepository
    • em.remove : 영속성을 활용하여 cascade로 연결되어 있는 객체들까지 한 번에 삭제 가능
    • em.remove(em.find(Board.class, id)) : id가 일치하는 board의 연관 객체들 전부 삭제
      • delete cascade from board_tb where id = ‘id’;
public void deleteById(Integer id) { em.remove(em.find(Board.class, id)); }
 

5. 글 수정

  • BoardController
@GetMapping("/board/{id}/update-form") public String updateForm(@PathVariable("id") Integer id, BoardRequest.UpdateDTO updateDTO, HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증이 필요합니다"); Board boardPS = boardService.글수정화면(updateDTO, id); request.setAttribute("model", boardPS); return "board/update-form"; } @PostMapping("/board/{id}/update") public String update(@PathVariable("id") Integer id, BoardRequest.UpdateDTO updateDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증이 필요합니다"); boardService.글수정(updateDTO, sessionUser.getId(), id); return "redirect:/board/"+id; }
 
  • BoardRequest
    • isPublic은 on 또는 null 값이 리턴됨
    • true/false 값을 받아야 하므로 이를 체크할 수 있는 isPublicChecked() 메서드 추가
      • String인 isPublic이 on이면 true, 아니면 false
@Data public static class UpdateDTO { private String title; private String content; private String isPublic; // "on" or null public Boolean isPublicChecked() { return "on".equals(isPublic); } }
 
  • BoardService
    • Update는 Dirty Checking을 사용하여 board 객체 내부 값을 setter로 변경
    • Transaction이 끝나면 자동 update
public Board 글수정화면(BoardRequest.UpdateDTO updateDTO, Integer id) { return boardRepository.findByIdJoinUserAndReplies(id); } @Transactional public void 글수정(BoardRequest.UpdateDTO updateDTO, Integer userId, Integer boardId) { // 1. 글 불러오기 by boardId Board board = boardRepository.findById(boardId); // 1-1. 글이 없으면 Exception404 if(board == null) throw new Exception404("해당하는 글이 없습니다."); // 2. 글 주인과 userId 비교 : 불일치 시 Exception403 if(!(board.getUser().getId().equals(userId))) throw new Exception403("권한이 없습니다."); // 3. update (dirty checking) board.update(updateDTO.getTitle(), updateDTO.getContent(), updateDTO.isPublicChecked()); }
 
  • BoardRepository
public Board findByIdJoinUserAndReplies(Integer id) { Query query = em.createQuery("select b from Board b join fetch b.user left join fetch b.replies r left join fetch r.user where b.id = :id order by r.id desc", Board.class); query.setParameter("id", id); return (Board) query.getSingleResult(); }
 
  • Board.update
public void update(String title, String content, Boolean isPublic) { this.title = title; this.content = content; this.isPublic = isPublic; }
 

결과

  • 제목, 내용, 공개 글 작성 체크박스 여부를 잘 반영함
    • 체크박스 해제 시 개인 글 설정
notion image
notion image
notion image
 

6. 글 페이징

  • Main 화면에 페이지 정보를 추가해야 함 : MainDTO 설계
@Data public static class MainDTO { private Integer prev; private Integer next; private Integer current; private Integer totalCount; private Integer totalPage; private Integer size; private Boolean isFirst; // currentPage == 0 private Boolean isLast; // totalCount, totalPage == currentPage private Integer pageSize; private Integer pageIndex; private List<Board> boards; private List<Integer> numbers; // 20개 [1, 2, 3, 4, 5, 6, 7] -> {{#model.numbers}} -> {{.}} public MainDTO(List<Board> boards, Integer current, Integer totalCount) { this.boards = boards; this.prev = current - 1; this.next = current + 1; this.size = 3; this.totalCount = totalCount; // given this.totalPage = makeTotalPage(totalCount, size); this.isFirst = current == 0; this.isLast = current == totalPage - 1; this.pageSize = 5; // 2-1. makeNumbers this.numbers = makeNumbers(current, pageSize, totalPage); this.pageIndex = 0; // 1. 페이지 전체 넣기 /* for (int i = 0; i < totalCount; i++) { this.numbers.add(i); } */ // 2. 페이지 묶음 for (int i = 0; i < pageSize; i++) { pageIndex = current / pageSize; // 현재 페이지 index : 페이지 목차 if (i % pageSize == 0) numbers.clear(); // 페이지를 불러올 때마다 페이지 리스트 클리어 int page = pageIndex * pageSize + i; // 페이지 변수 : 페이지 index * pageSize + i // 0 ~ 4 : 0 * 5 + 0~4, 5 ~ 9 : 1 * 5 + 0~4, ... if (page < totalPage) numbers.add(page); // 최대 페이지 전까지 추가 } } // 2-1. makeNumbers private List<Integer> makeNumbers(int current, int pageSize, int totalPage) { List<Integer> numbers = new ArrayList<>(); int start = (current / pageSize) * pageSize; int end = Math.min(start + pageSize, totalPage); for (int i = start; i < end; i++) { numbers.add(i); } return numbers; } private Integer makeTotalPage(int totalCount, int size) { return totalCount / size + (totalCount % size == 0 ? 0 : 1); } }

글 페이징

  • 현재 페이지 정보를 불러오기 위해, 글 목록 보기를 일부 개수만큼 잘라서 들고 와야 함
    • 3 개씩 들고 옴 : size에 저장
    • 페이징 사이즈 갯수는 여러 곳에 재사용할 수 있도록 추후 final static 선언이 필요
  • 이때, board 객체에는 3개의 글밖에 담기지 않으므로, 쿼리를 통한 총 board 개수가 필요
    • 새 쿼리 생성 필요
  • 페이지의 목차가 필요
    • 0 ~ 4 / 5 ~ 9 / 10 ~ 14 / … → pageIndex * pageSize + 0 ~ 4
  • 페이지를 담을 list 설계 : List<Integer> numbers
    • 페이지 index = 현재 페이지 / 페이지 목록 크기
    • 페이지는 1번만 초기화해야 함
      • 나머지가 0이면 초기화 : i가 0일 때 (맨 처음에만) 초기화
    • 페이지 계산 : 모듈화 필요
      • 0 ~ 4 : 페이지 index=0이므로 0 * 5= 0 → 0 + (0 ~ 4)
      • 5 ~ 9 : 페이지 index=1이므로 1 * 5 = 5 → 5 + (0 ~ 4)
      • 10 ~ 14 : 페이지 index=2이므로 2 * 5 = 10 → 10 + (0 ~ 4)
    • 페이지가 총 페이지 수보다 작을 때만 추가
 
  • BoardController
    • requestParameter로 현재 페이지를 받아와야 함
@GetMapping("/") public String list(HttpServletRequest request, @RequestParam(required = false, value = "page", defaultValue = "0") Integer page) { User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { request.setAttribute("model", boardService.글목록보기(null, page)); } else { request.setAttribute("model", boardService.글목록보기(sessionUser.getId(), page)); } return "board/list"; }
 
  • BoardService
    • MainDTO 반환 : 페이지 정보가 추가됨
public BoardResponse.MainDTO 글목록보기(Integer userId, Integer page) { if (userId == null) { return new BoardResponse.MainDTO(boardRepository.findAll(page), page, boardRepository.totalCount().intValue()); } else { return new BoardResponse.MainDTO(boardRepository.findAll(userId, page), page, boardRepository.totalCount(userId).intValue()); } }
 
  • BoardRepository
    • findAll
      • setFirstResult(n) → 데이터 인덱스 (어디서부터 찾을건지 확인)
      • setMaxResults(n) → 최대 표시 개수 (board 출력 개수)
    • totalCount
      • count(*)로 총 board 갯수 반환
// localhost:8080?page=0 public List<Board> findAll(int page) { String sql = "select b from Board b where b.isPublic = true order by b.id desc"; Query query = em.createQuery(sql, Board.class); query.setFirstResult(page * 3); query.setMaxResults(3); return query.getResultList(); } public List<Board> findAll(Integer userId, int page) { String sql = "select b from Board b where b.isPublic = true or b.user.id = :userId order by b.id desc"; Query query = em.createQuery(sql, Board.class); query.setParameter("userId", userId); query.setFirstResult(page * 3); query.setMaxResults(3); return query.getResultList(); } // 그룹 함수 : Long 리턴 // 1. 로그인 안 했을 때 : 4개 public Long totalCount() { Query q = em.createQuery("select count(b) from Board b where b.isPublic = true", Long.class); return (Long) q.getSingleResult(); } // 2-1. ssar로 로그인 했을 때 : 5개 // 2-2. cos로 로그인 했을 때 : 4개 public Long totalCount(int userId) { Query q = em.createQuery("select count(b) from Board b where b.isPublic = true or b.user.id = :userId", Long.class); q.setParameter("userId", userId); return (Long) q.getSingleResult(); }
 
  • list.mustache
<ul class="pagination d-flex justify-content-center"> <li class="page-item {{#model.isFirst}} disabled {{/model.isFirst}}"><a class="page-link" href="?page={{model.prev}} ">Previous</a></li> {{#model.numbers}} <li class="page-item"><a class="page-link" href="?page={{.}}">{{.}}</a></li> {{/model.numbers}} <li class="page-item {{#model.isLast}} disabled {{/model.isLast}}"><a class="page-link" href="?page={{model.next}}">Next</a></li> </ul>
 

결과

  • 현재 페이지(default)가 0이므로 Previous는 비활성화, 0 ~ 4까지 페이지 번호 출력
notion image
 
  • 5페이지가 넘어가면, 페이지 일부만 출력 (글이 20개이므로, 5 ~ 6까지만 출력)
notion image
 

7. 글 검색

  • BoardController
@GetMapping("/") public String list(HttpServletRequest request, @RequestParam(required = false, value = "page", defaultValue = "0") Integer page, @RequestParam(required = false, value = "keyword", defaultValue = "") String keyword) { User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { request.setAttribute("model", boardService.글목록보기(null, page, keyword)); } else { request.setAttribute("model", boardService.글목록보기(sessionUser.getId(), page, keyword)); } System.out.println("keyword : " + keyword); return "board/list"; }
 
  • BoardResponse
@Data public static class MainDTO { private Integer prev; private Integer next; private Integer current; private Integer totalCount; private Integer totalPage; private Integer size; private Boolean isFirst; // currentPage == 0 private Boolean isLast; // totalCount, totalPage == currentPage private Integer pageSize; private String keyword; private List<Board> boards; private List<Integer> numbers; // 20개 [1, 2, 3, 4, 5, 6, 7] -> {{#model.numbers}} -> {{.}} public MainDTO(List<Board> boards, Integer current, Integer totalCount, String keyword) { this.boards = boards; this.prev = current - 1; this.next = current + 1; this.size = 3; this.totalCount = totalCount; // given this.totalPage = makeTotalPage(totalCount, size); this.isFirst = current == 0; this.isLast = current == totalPage - 1; this.pageSize = 5; this.numbers = makeNumbers(current, pageSize, totalPage); // keyword 값이 있거나 null this.keyword = keyword; // 1. 페이지 전체 넣기 /* for (int i = 0; i < totalCount; i++) { this.numbers.add(i); } */ // 2. 페이지 묶음 /*for (int i = 0; i < pageSize; i++) { pageIndex = current / pageSize; // 현재 페이지 index : 페이지 목차 if (i % pageSize == 0) numbers.clear(); // 페이지를 불러올 때마다 페이지 리스트 클리어 int page = pageIndex * pageSize + i; // 페이지 변수 : 페이지 index * pageSize + i // 0 ~ 4 : 0 * 5 + 0~4, 5 ~ 9 : 1 * 5 + 0~4, ... if (page < totalPage) numbers.add(page); // 최대 페이지 전까지 추가 }*/ }
 
  • BoardService
public BoardResponse.MainDTO 글목록보기(Integer userId, Integer page, String keyword) { if (userId == null) { return new BoardResponse.MainDTO(boardRepository.findAll(page, keyword), page, boardRepository.totalCount(keyword).intValue(), keyword); } else { return new BoardResponse.MainDTO(boardRepository.findAll(userId, page, keyword), page, boardRepository.totalCount(userId, keyword).intValue(), keyword); } }
 
  • BoardRepository
// localhost:8080?page=0 public List<Board> findAll(int page, String keyword) { String sql = ""; if (!(keyword.isBlank())) sql += "select b from Board b where b.isPublic = true and b.title like :keyword order by b.id desc"; else sql += "select b from Board b where b.isPublic = true order by b.id desc"; Query query = em.createQuery(sql, Board.class); // keyword를 포함 : title like %keyword% if (!(keyword.isBlank())) query.setParameter("keyword", "%" + keyword + "%"); query.setFirstResult(page * 3); query.setMaxResults(3); return query.getResultList(); } public List<Board> findAll(Integer userId, int page, String keyword) { String sql = ""; if (!(keyword.isBlank())) sql += "select b from Board b where b.isPublic = true or b.user.id = :userId and b.title like :keyword order by b.id desc"; else sql += "select b from Board b where b.isPublic = true or b.user.id = :userId order by b.id desc"; Query query = em.createQuery(sql, Board.class); // keyword를 포함 : title like %keyword% if (!(keyword.isBlank())) query.setParameter("keyword", "%" + keyword + "%"); query.setParameter("userId", userId); query.setFirstResult(page * 3); query.setMaxResults(3); return query.getResultList(); } // 그룹 함수 : Long 리턴 // 1. 로그인 안 했을 때 : 4개 public Long totalCount(String keyword) { String sql = ""; if (!(keyword.isBlank())) sql += "select count(b) from Board b where b.isPublic = true and b.title like :keyword"; else sql += "select count(b) from Board b where b.isPublic = true"; Query query = em.createQuery(sql, Long.class); // keyword를 포함 : title like %keyword% if (!(keyword.isBlank())) query.setParameter("keyword", "%" + keyword + "%"); return (Long) query.getSingleResult(); } // 2-1. ssar로 로그인 했을 때 : 5개 // 2-2. cos로 로그인 했을 때 : 4개 public Long totalCount(int userId, String keyword) { String sql = ""; if (!(keyword.isBlank())) sql += "select count(b) from Board b where b.isPublic = true or b.user.id = :userId and b.title like :keyword"; else sql += "select count(b) from Board b where b.isPublic = true or b.user.id = :userId"; Query query = em.createQuery(sql, Long.class); // keyword를 포함 : title like %keyword% if (!(keyword.isBlank())) query.setParameter("keyword", "%" + keyword + "%"); query.setParameter("userId", userId); return (Long) query.getSingleResult(); }
 

결과

  • keyword 검색 시 keyword가 제목에 포함된 글을 찾아 출력, 페이징도 동작 확인
notion image
Share article

sxias