게시판 기능 구현 6 : 조회조건 - 스프링, jsp, 오라클, mybatis
게시판 홈에서 아래와 같이 좋아요순으로 게시글을 조회하기, 조회수순으로 게시글을 조회하기 두 기능을 구현해볼 것이다.
그리고, 게시판 홈에서, 아래와 같이, 제목 또는 작성자 또는 내용으로 게시물을 조회할 수 있도록 해두었는데, 이를 구현해볼 것이다.
----------------------------------------------------------------------------------------------------------------------
우선, 간단한 조회수순 조회, 좋아요순 조회를 구현해보자.
일단, 이전에 만들었던 것이 최신순으로 게시글을 조회하는 거였거든?
이 부분을 다시 복기해보자.
localhost:8080/board-home 으로 get 요청을 보내면, 아래 컨트롤러가 받게 되고,
@Controller
@RequiredArgsConstructor
@Slf4j
public class BoardHomeController {
private final BoardHomeService boardHomeService;
@GetMapping("/board-home")
public String boardHomeControllerMethod(@RequestParam(required = false, defaultValue = "1") Integer page, Model model, @RequestParam(required = false) String hecker) {
if(hecker != null){
model.addAttribute("hecker", hecker);
}
boardHomeService.findAllPosts(page, model);
return "/contact/boardHome";
}
}
이 컨트롤러에서 boardHome.jsp 파일로 가기 전에,
BoardHomeService 라는 클래스의 findAllPosts 라는 메서드를 호출했었다.
그리고, findAllPosts 에서는 아래와 같이 6가지 데이터를 model 에 담아줬었다.
private final PostsMapper postsMapper;
private final PaginationService paginationService;
private final ViewNumberChangeFromNullToZeroService viewNumberChangeFromNullToZeroService;
public void findAllPosts(Integer page, Model model){
Integer currentPage = page; // 현재 페이지
Integer pageSize = 20; // 페이지당 보여질 post 의 수
Integer totalPosts = postsMapper.findAllPostsCount(); // 모든 post 의 개수
Integer pageGroupSize = 9; // 그룹당 페이지의 개수
// 현재 페이지가 보여줘야 하는 PostsDTO 객체들을 담은 List자료구조
int startRow = (currentPage - 1) * pageSize;
List<PostsDTO> currentPagePosts = postsMapper.findCurrentPagePosts(startRow, pageSize);
// 게시판 홈에서 현재 조회수가 없더라도 비어있지 않고 0 이 입력되도록 하는 서비스
List<PostsDTO> currentPagePostsChanged = viewNumberChangeFromNullToZeroService.viewNumberChange(currentPagePosts);
//currentPage,pageSize,totalWritings, pageGroupSize, currentPagePosts
paginationService.pagination(currentPage,pageSize,totalPosts, pageGroupSize, currentPagePostsChanged, model);
}
최신순, 조회수순, 좋아요순 이 3가지의 차이점은 어디에서 발생할까?
findAllPosts 에서
List<PostsDTO> currentPagePosts = postsMapper.findCurrentPagePosts(startRow, pageSize);
이 부분이 이제 db 에서 나열할 게시글들을 조회하는 부분이거든?
그리고, 아래 부분이 findCurrentPagePosts 라는 메서드를 호출하면, 실행될 쿼리이다.
<select id="findCurrentPagePosts" resultType="PostsDTO">
SELECT posts.id, posts.member_id, posts.title, posts.is_private, TO_CHAR(posts.writing_date, 'YYYY-MM-DD') writing_date, posts.content, posts.delete_post_fl, post_view.view_number, member.user_id
FROM posts
LEFT OUTER JOIN post_view ON posts.id = post_view.post_id
JOIN member ON posts.member_id = member.id
WHERE posts.DELETE_POST_FL = 'N'
ORDER BY posts.id DESC
OFFSET #{startRow} ROWS
FETCH NEXT #{pageSize} ROWS ONLY
</select>
위 쿼리에서, ORDER BY 부분을 posts 테이블의 id 로 하지 않고,
POST_LIKE 테이블의 like_number 컬럼 순으로 정렬하라고 하면 좋아요 순으로 현재 페이지에 보여질 게시글들이 정렬되어 조회될 것이다. 아래 처럼 말이다.
또한, 최신순으로 조회할 때 정렬의 기준으로 삼았던 posts 테이블의 id 컬럼값은 null 값이 없었던 반면,
좋아요순으로 조회할 때 정렬의 기준으로 삼은 post_like 테이블의 like_number 컬럼은 아직 좋아요 를 아무도 누르지 않은 경우라면, null 일 수 있다. 정렬할 때, null 값이 있는 경우, 조회결과가 이상하게 되기 때문에, NULLS LAST 를 붙여서 null 값이 제일 마지막에 조회되도록 하였다.
<!-- 좋아요 순 조회 -->
<select id="findCurrentPagePostsOrderByLikeNum" resultType="PostsDTO">
SELECT posts.id, posts.member_id, posts.title, posts.is_private, TO_CHAR(posts.writing_date, 'YYYY-MM-DD') writing_date, posts.content, posts.delete_post_fl, post_view.view_number, member.user_id
FROM posts
LEFT OUTER JOIN post_view ON posts.id = post_view.post_id
LEFT OUTER JOIN member ON posts.member_id = member.id
LEFT OUTER JOIN post_like ON posts.id = post_like.post_id
WHERE posts.DELETE_POST_FL = 'N'
ORDER BY post_like.like_number DESC NULLS LAST
OFFSET #{startRow} ROWS
FETCH NEXT #{pageSize} ROWS ONLY
</select>
또한, POST_VIEW 테이블의 view_number 컬럼 순으로 정렬하라고 하면 조회수 순으로 현재 페이지에 보여질 게시글들이 정렬되어 조회될 것이다. 아래 처럼 말이다. 이 select 문에서도 정렬의 기준이 되는 컬럼인 post_view 테이블의 view_number 컬럼이 null값이 될 수 있기 때문에, NULLS LAST 를 붙여주었다.
<!-- 조회수 순 조회 -->
<select id="findCurrentPagePostsOrderByViewNum" resultType="PostsDTO">
SELECT posts.id, posts.member_id, posts.title, posts.is_private, TO_CHAR(posts.writing_date, 'YYYY-MM-DD') writing_date, posts.content, posts.delete_post_fl, post_view.view_number, member.user_id
FROM posts
LEFT OUTER JOIN post_view ON posts.id = post_view.post_id
LEFT OUTER JOIN member ON posts.member_id = member.id
WHERE posts.DELETE_POST_FL = 'N'
ORDER BY post_view.view_number DESC NULLS LAST
OFFSET #{startRow} ROWS
FETCH NEXT #{pageSize} ROWS ONLY
</select>
그래서, boardHome.jsp 파일에서
<a href="/board-home" class="post-make" > 최신순 </a>
<a href="/board-home-order-by-like-num" class="post-make" > 좋아요순 </a>
<a href="/board-home-order-by-view-num" class="post-make" > 조회수순 </a>
이렇게 만들어두고, 클릭하면 아래 컨트롤러들이 매핑하면,
@Controller
@RequiredArgsConstructor
@Slf4j
public class BoardHomeController {
private final BoardHomeService boardHomeService;
private final BoardHomeRecommendService boardHomeRecommendService;
@GetMapping("/board-home")
public String boardHomeControllerMethod(@RequestParam(required = false, defaultValue = "1") Integer page, Model model, @RequestParam(required = false) String hecker) {
if(hecker != null){
model.addAttribute("hecker", hecker);
}
boardHomeService.findAllPosts(page, model);
return "/contact/boardHome";
}
@GetMapping("/board-home-order-by-like-num")
public String boardHomeOrderByLikeNum (@RequestParam(required = false, defaultValue = "1") Integer page, Model model){
boardHomeService.findAllPosts2(page, model);
return "/contact/boardHome";
}
@GetMapping("/board-home-order-by-view-num")
public String boardHomeOrderByViewNum (@RequestParam(required = false, defaultValue = "1") Integer page, Model model){
boardHomeService.findAllPosts3(page, model);
return "/contact/boardHome";
}
}
아래와 같이, 비슷하지만 다른 서비스계층의 코드들(BoardHomeService 클래스들의 메서드들)이 실행되도록 한 다음,
@Service
@RequiredArgsConstructor
@Slf4j
//@Transactional 안쓴다. 모두 select 쿼리니까.
public class BoardHomeService {
private final PostsMapper postsMapper;
private final PaginationService paginationService;
private final ViewNumberChangeFromNullToZeroService viewNumberChangeFromNullToZeroService;
public void findAllPosts(Integer page, Model model){
Integer currentPage = page; // 현재 페이지
Integer pageSize = 20; // 페이지당 보여질 post 의 수
Integer totalPosts = postsMapper.findAllPostsCount(); // 모든 post 의 개수
Integer pageGroupSize = 9; // 그룹당 페이지의 개수
// 현재 페이지가 보여줘야 하는 PostsDTO 객체들을 담은 List자료구조
int startRow = (currentPage - 1) * pageSize;
List<PostsDTO> currentPagePosts = postsMapper.findCurrentPagePosts(startRow, pageSize);
// 게시판 홈에서 현재 조회수가 없더라도 비어있지 않고 0 이 입력되도록 하는 서비스
List<PostsDTO> currentPagePostsChanged = viewNumberChangeFromNullToZeroService.viewNumberChange(currentPagePosts);
//currentPage,pageSize,totalWritings, pageGroupSize, currentPagePosts
paginationService.pagination(currentPage,pageSize,totalPosts, pageGroupSize, currentPagePostsChanged, model);
}
public void findAllPosts2(Integer page, Model model){
Integer currentPage = page; // 현재 페이지
Integer pageSize = 20; // 페이지당 보여질 post 의 수
Integer totalPosts = postsMapper.findAllPostsCount(); // 모든 post 의 개수
Integer pageGroupSize = 9; // 그룹당 페이지의 개수
// 현재 페이지가 보여줘야 하는 PostsDTO 객체들을 담은 List자료구조
int startRow = (currentPage - 1) * pageSize;
List<PostsDTO> currentPagePosts = postsMapper.findCurrentPagePostsOrderByLikeNum(startRow, pageSize);
// 게시판 홈에서 현재 조회수가 없더라도 비어있지 않고 0 이 입력되도록 하는 서비스
List<PostsDTO> currentPagePostsChanged = viewNumberChangeFromNullToZeroService.viewNumberChange(currentPagePosts);
//currentPage,pageSize,totalWritings, pageGroupSize, currentPagePosts
paginationService.pagination(currentPage,pageSize,totalPosts, pageGroupSize, currentPagePostsChanged, model);
}
public void findAllPosts3(Integer page, Model model){
Integer currentPage = page; // 현재 페이지
Integer pageSize = 20; // 페이지당 보여질 post 의 수
Integer totalPosts = postsMapper.findAllPostsCount(); // 모든 post 의 개수
Integer pageGroupSize = 9; // 그룹당 페이지의 개수
// 현재 페이지가 보여줘야 하는 PostsDTO 객체들을 담은 List자료구조
int startRow = (currentPage - 1) * pageSize;
List<PostsDTO> currentPagePosts = postsMapper.findCurrentPagePostsOrderByViewNum(startRow, pageSize);
// 게시판 홈에서 현재 조회수가 없더라도 비어있지 않고 0 이 입력되도록 하는 서비스
List<PostsDTO> currentPagePostsChanged = viewNumberChangeFromNullToZeroService.viewNumberChange(currentPagePosts);
//currentPage,pageSize,totalWritings, pageGroupSize, currentPagePosts
paginationService.pagination(currentPage,pageSize,totalPosts, pageGroupSize, currentPagePostsChanged, model);
}
}
아까 그 쿼리들이 아래와 같이 실행되도록 하면 된다. 참고로, 위 코드 중 나머지 코드들에 대해서는 내 포스팅 중 게시판 기능 구현 3 : 게시판 홈(게시글 목록 페이지) 만들기 - 스프링, jsp, 오라클, mybatis 를 보면 된다.
<!--최신순 조회 -->
<select id="findCurrentPagePosts" resultType="PostsDTO">
SELECT posts.id, posts.member_id, posts.title, posts.is_private, TO_CHAR(posts.writing_date, 'YYYY-MM-DD') writing_date, posts.content, posts.delete_post_fl, post_view.view_number, member.user_id
FROM posts
LEFT OUTER JOIN post_view ON posts.id = post_view.post_id
JOIN member ON posts.member_id = member.id
WHERE posts.DELETE_POST_FL = 'N'
ORDER BY posts.id DESC
OFFSET #{startRow} ROWS
FETCH NEXT #{pageSize} ROWS ONLY
</select>
<!-- 좋아요 순 조회 -->
<select id="findCurrentPagePostsOrderByLikeNum" resultType="PostsDTO">
SELECT posts.id, posts.member_id, posts.title, posts.is_private, TO_CHAR(posts.writing_date, 'YYYY-MM-DD') writing_date, posts.content, posts.delete_post_fl, post_view.view_number, member.user_id
FROM posts
LEFT OUTER JOIN post_view ON posts.id = post_view.post_id
LEFT OUTER JOIN member ON posts.member_id = member.id
LEFT OUTER JOIN post_like ON posts.id = post_like.post_id
WHERE posts.DELETE_POST_FL = 'N'
ORDER BY post_like.like_number DESC NULLS LAST
OFFSET #{startRow} ROWS
FETCH NEXT #{pageSize} ROWS ONLY
</select>
<!-- 조회수 순 조회 -->
<select id="findCurrentPagePostsOrderByViewNum" resultType="PostsDTO">
SELECT posts.id, posts.member_id, posts.title, posts.is_private, TO_CHAR(posts.writing_date, 'YYYY-MM-DD') writing_date, posts.content, posts.delete_post_fl, post_view.view_number, member.user_id
FROM posts
LEFT OUTER JOIN post_view ON posts.id = post_view.post_id
LEFT OUTER JOIN member ON posts.member_id = member.id
WHERE posts.DELETE_POST_FL = 'N'
ORDER BY post_view.view_number DESC NULLS LAST
OFFSET #{startRow} ROWS
FETCH NEXT #{pageSize} ROWS ONLY
</select>
----------------------------------------------------------------------------------------------------------------------
이제, 제목 또는 작성자 또는 내용으로 게시글을 조회하는 것을 구현해보자.
혹시 몰라서 말하는데, 위 부분은 게시판홈 인 boardHome.jsp 파일에 있던 것이다.
아래 부분은 boardHome.jsp 파일이 렌더링되어 있는 모습이다.
boardHome.jsp 파일 중 이 부분의 코드는 아래와 같다.
<form action="/find-posts-by-title-writer-content" method="post" id="find-writing-form">
<select name="byWhatType">
<option value="title">제목</option>
<option value="writer">작성자</option>
<option value="content">내용</option>
</select>
<input type="text" name="hintToFind" id="hint-to-find"/>
<button type="submit"> 찾기 </button>
</form>
'찾기' 버튼을 누르면, 빈 내용으로 게시글을 찾으려고 했는지 javascript 도 검증해준다.
만약, 빈 내용으로 게시글을 찾으려고 했으면, '내용을 입력해주세요!' 라고 alert 창을 띄우고, 빈 내용이 아니라면, form 을 전송해준다.
<script>
document.addEventListener("DOMContentLoaded", function() {
// 폼 선택
const form = document.querySelector('#find-writing-form');
let hintToFind = document.querySelector('#hint-to-find');
form.addEventListener('submit', function(event) {
event.preventDefault(); // 폼의 기본 제출 동작을 방지
// 만약 아무것도 입력안하고 찾기 버튼을 눌렀을 경우, alert()창으로 내용을 입력해주세요! 라고 안내하고, return.
if(hintToFind.value.trim() == '' ){
alert('내용을 입력해주세요!');
return;
} else{
// 만약 내용을 입력했다면, form 전송.
form.submit();
}
});
});
</script>
그렇게 해서 폼이 제출되면, 받을 컨트롤러는 아래와 같다.
@Controller
@RequiredArgsConstructor
@Slf4j
public class FindPostsByTitleWriterContentController {
private final FindPostsByTitleWriterService findPostsByTitleWriterService;
@RequestMapping("find-posts-by-title-writer-content")
public String findWritingControllerMethod(@RequestParam String byWhatType, @RequestParam String hintToFind, @RequestParam(required = false, defaultValue = "1") Integer page, Model model) {
findPostsByTitleWriterService.findPostsService(byWhatType, hintToFind, page, model);
return "/contact/boardHomeFind";
}
}
폼으로 보내준 데이터에 대해서 설명하자면,
byWhatType 은 제목으로 검색하려 했는지(title), 작성자로 검색하려 했는지(writer), 내용으로 검색하려 했는지(content)를 나타내주는 데이터이다.
그리고, hintToFind 는 사용자가 입력한 input 태그 값이다. 예를 들어, 제목으로 'a' 라는 게시글을 찾는다고 했다면, 'a' 가 이 hintToFind 에 해당한다.
그리고, page 는 조회된 게시글들이 많을 경우, 페이지네이션을 해주기 위해서, 필요한 정보인데, 지금은 defaultValue 인 1 이 바인딩되어 있다.
메서드 본문을 보면, FindPostsByTitleWriterService 라는 클래스의 findPostsService 라는 메서드를 호출하고 나서, boardHomeFind.jsp 파일로 제어권을 넘긴다.
findPostsService 라는 메서드는 아래와 같다.
@Service
//@Transactional 안쓴다. 모두 select 쿼리니까.
@RequiredArgsConstructor
@Slf4j
public class FindPostsByTitleWriterService {
private final PostsMapper postsMapper;
private final MemberMapper memberMapper;
private final PaginationService paginationService;
public void findPostsService(String byWhatType, String hintToFind, Integer page, Model model){
Integer currentPage = page; //현재 페이지
// 총 페이지 수 구하기
Integer totalPosts = 0; // 총 게시글 수를 담을 변수를 만들어둠.
// 사용자가 제목으로 조회한 경우, 그 제목과 일치하는 게시글의 개수를 조회하기
if (byWhatType.equals("title")) {
totalPosts = postsMapper.findPostCountByTitle(hintToFind);
}
// 사용자가 작성자로 조회한 경우, 그 작성자가 작성한 게시글의 개수를 조회하기
if (byWhatType.equals("writer")) {
MemberJoinDTO findMember = memberMapper.findMemberById(hintToFind);
Integer id = findMember.getId();
totalPosts = postsMapper.findPostCountByMemberId(id);
}
// 사용자가 내용으로 조회한 경우, 그 내용을 담고 있는 게시글의 개수를 조회하기
if (byWhatType.equals("content")) {
totalPosts = postsMapper.findPostCountByContent(hintToFind);
}
Integer pageSize = 5; // 페이지당 보여질 게시글 의 수
// 총 페이지 수 = 총 게시글 수(totalPosts) / 페이지당 보여질 게시글의 수(pageSize)
Integer totalPages = (int) Math.ceil((double) totalPosts / pageSize);
Integer pageGroupSize = 5; // 몇개의 페이지를 하나의 그룹으로 묶었는지.
//----------------------------------------------------------------------------------
//현재 페이지에 보여질 게시글(PostsDTO) 을 담은 List 자료구조
Integer startRow = (currentPage - 1) * pageSize;
List<PostsDTO> currentPagePosts = new ArrayList<>();
// 사용자가 제목으로 조회한 경우.
if (byWhatType.equals("title")) { // hintToFind, startRow, pageSize
currentPagePosts = postsMapper.findPostsByTitle(hintToFind, startRow, pageSize);
}
// 사용자가 작성자로 조회한 경우.
if (byWhatType.equals("writer")) {
MemberJoinDTO findMember = memberMapper.findMemberById(hintToFind);
Integer id = findMember.getId();
currentPagePosts = postsMapper.findPostsByMemberId(id, startRow, pageSize);
}
// 사용자가 내용으로 조회한 경우.
if (byWhatType.equals("content")) {
currentPagePosts = postsMapper.findPostsByContent(hintToFind, startRow, pageSize);
}
// 만약 조회된 게시글이 하나도 없다면, "조회된 게시글이 없습니다" 라는 문구를 표시하기 위해서, model 에 담아줌.
if (currentPagePosts.size() == 0) {
model.addAttribute("nothing", "조회된 게시글이 없습니다.");
}
//boardHomeFind.jsp 파일에서 사용자가 입력한 byWhatType 과 hintToFind 를 그대로 보여주기 위해서 model 에 넣어둠
model.addAttribute("byWhatType", byWhatType);
model.addAttribute("hintToFind", hintToFind);
paginationService.pagination(currentPage, pageSize, totalPosts, pageGroupSize, currentPagePosts, model);
}
}
이 메서드에서 해주는 것은,
게시판 기능 구현 3 : 게시판 홈(게시글 목록 페이지) 만들기 - 스프링, jsp, 오라클, mybatis
에서 해줬었던 것과 다르지 않다.
6가지 재료를 모아서, 넘겨주면 된다.
1. 현재 페이지에 보여질 게시글(PostsDTO)을 담은 List자료구조.
2. 현재 boardHome.jsp 파일에서 렌더링하고 있는 페이지가 "몇 페이지" 인지.
3. 총 페이지 수
4. 현재 페이지 그룹의 첫번째 페이지
5. 현재 페이지 그룹의 마지막 페이지
6. 몇개의 페이지를 하나의 그룹으로 묶었는지.
이렇게 6가지였는데, 하나씩 재료를 모아서 model 에 담아주면 된다.
2. 현재 boardHome.jsp 파일에서 렌더링하고 있는 페이지가 "몇 페이지" 인지.
부터 보면,
Integer currentPage = page; //현재 페이지
이 부분이 현재 페이지를 나타낸다. 현재는 defaultValue 였던 1 이 currentPage 라는 변수에 담겨있다.
3. 총 페이지 수
총 페이지 수는 다음과 같이 구한다. '총 게시글수'를 '페이지당 보여줄 게시글의 수' 로 나눈 값.
그래서, 일단, 총 게시글의 수를 구해야 하는데, 그 부분이 아래 코드이다.
Integer totalPosts = 0; // 총 게시글 수를 담을 변수를 만들어둠.
// 사용자가 제목으로 조회한 경우, 그 제목과 일치하는 게시글의 개수를 조회하기
if (byWhatType.equals("title")) {
totalPosts = postsMapper.findPostCountByTitle(hintToFind);
}
// 사용자가 작성자로 조회한 경우, 그 작성자가 작성한 게시글의 개수를 조회하기
if (byWhatType.equals("writer")) {
MemberJoinDTO findMember = memberMapper.findMemberById(hintToFind);
Integer id = findMember.getId();
totalPosts = postsMapper.findPostCountByMemberId(id);
}
// 사용자가 내용으로 조회한 경우, 그 내용을 담고 있는 게시글의 개수를 조회하기
if (byWhatType.equals("content")) {
totalPosts = postsMapper.findPostCountByContent(hintToFind);
}
사용자가 제목으로 조회하려고 하는 경우, POSTS 테이블에서 hintToFind에 담겨있는 값을 제목에 포함하는 게시글의 수를 조회하는 것이다.
findPostCountByTitle 이 호출되면 실행될 쿼리는 아래와 같다.
// PostsMapper 라는 @Mapper 안
public Integer findPostCountByTitle(String hintToFind);
// PostsMapper.xml
<select id="findPostCountByTitle" resultType="Integer">
SELECT COUNT(*)
FROM posts
WHERE title LIKE '%' || #{hintToFind} || '%'
AND DELETE_POST_FL = 'N'
</select>
그리고 사용자가 작성자로 조회하려는 경우에는, POSTS 테이블에서 member_id 컬럼은 MEMBER 테이블의 user_id 컬럼값이 아니라, MEMBER 테이블의 id(PK)컬럼값이기 때문에, 일단, MEMBER 테이블에서 hintToFind( 사용자가 input 태그에 입력한 값으로서, byWhatType 이 writer 이기 때문에, 조회할 작성자의 member테이블 user_id 컬럼값) 와 일치하는 행을 가져와서, 그 행의 id 라는 필드에 담긴 값을 가지고, POSTS 테이블에서 그 id라는 필드에 담긴 값이 POSTS 테이블의 member_id 컬럼값과 일치하는 행의 개수를 조회하는 것이다.
MemberMapper 라는 인터페이스(@Mapper인터페이스) 안 findMemberById 라는 메서드와 MemberMapper.xml 파일 안 findMemberById 는 아래와 같다.
//MemberMapper (매퍼 인터페이스)
MemberJoinDTO findMemberById(String userId);
//MemberMapper.xml
<select id="findMemberById" resultType="MemberJoinDTO">
SELECT * FROM member
WHERE USER_ID = #{userId}
AND DELETE_MEMBER_FL = 'N'
</select>
PostsMapper 라는 인터페이스 안은 아래와 같다.
//매퍼
public Integer findPostCountByMemberId(Integer memberId);
//xml
<select id="findPostCountByMemberId" resultType="Integer">
SELECT COUNT(*)
FROM posts
WHERE member_id = #{hintToFind}
AND DELETE_POST_FL = 'N'
</select>
그리고 사용자가 내용으로 조회할 경우 실행될 쿼리는 아래와 같다.
//PostsMapper(@Mapper 붙은 인터페이스)
public Integer findPostCountByContent (String content);
//PostsMapper.xml
<select id="findPostCountByContent" resultType="Integer">
SELECT COUNT(*)
FROM posts
WHERE content LIKE '%' || #{hintToFind} || '%'
AND DELETE_POST_FL = 'N'
</select>
이렇게 해서, 총게시글의 수를 구했다. 이제 '페이지당 보여줄 게시글의 수' 정하면 된다.
그게 아래 코드이다.
Integer pageSize = 5; // 페이지당 보여질 게시글 의 수
// 총 페이지 수 = 총 게시글 수(totalPosts) / 페이지당 보여질 게시글의 수(pageSize)
Integer totalPages = (int) Math.ceil((double) totalPosts / pageSize);
다음은, 6. 몇개의 페이지를 하나의 그룹으로 묶었는지. 를 정해야 한다. 그게 아래 코드이다.
Integer pageGroupSize = 5; // 몇개의 페이지를 하나의 그룹으로 묶었는지.
4. 현재 페이지 그룹의 첫번째 페이지
5. 현재 페이지 그룹의 마지막 페이지
이 두 개는
paginationService.pagination(currentPage, pageSize, totalPosts, pageGroupSize, currentPagePosts, model);
이 메서드를 실행시키면, 알아서 되도록 했었다. pagination 메서드는 아래와 같았었다.
currentGroup (현재 페이지가 속한 "페이지그룹") 은 어떻게 구하냐면,
Integer currentGroup = (int) Math.ceil((double) currentPage / pageGroupSize);
현재 페이지(currentPage)가 예를 들어 13페이지이고, 페이지를 9개씩 그룹화 한다고 했을 때,
13/9 = 1.xxx 가 나올 것이다. 이를 무조건 올림처리해서 2로 만드는 것이다. 따라서, 이 경우의 currentGroup(현재 페이지가 속한 그룹) 은 2 이다.
이렇게 해서 currentGroup 을 구한 다음에, 이제 현재 페이지가 속한 그룹의 첫번째 페이지를 구하는거야.
어떻게 구하냐면, (현재 페이지가 속한 그룹 - 1) * pageGroupSize + 1
이렇게 구하면 된다.
현재 페이지가 속한 그룹이 2 이고, 페이지를 9개씩 묶었다면,
( 2 -1 ) * 9 + 1 = 10
이렇게 10페이지가 현재 페이지가 속한 그룹의 첫번째 페이지라는 걸 구할 수 있는 것이다.
현재 페이지가 속한 그룹의 마지막 페이지는 어떻게 구하냐면,
현재 페이지가 속한 그룹의 첫번째 페이지 + 페이지를 몇개씩 묶었는지 - 1
이렇게 계산하면 된다.
예를 들어, 현재 페이지가 속한 그룹의 첫번째 페이지가 10 이고 페이지를 9개씩 묶었다면,
10 + 9 - 1 = 18
18 페이지가 현재 페이지가 속한 그룹의 마지막 페이지가 되는 것이다.
주의할 건, 마지막페이지와 비교하여 더 작은 것이 현재 페이지가 속한 그룹의 마지막 페이지가 되어야 한다는 것이다.
@Service
// @Transactional 안쓴다. db관련 코드 없음.
public class PaginationService {
public void pagination (Integer currentPage, Integer pageSize, Integer totalPosts, Integer pageGroupSize, List<?> currentPagePosts, Model model){
Integer totalPages = (int) Math.ceil((double) totalPosts / pageSize);
Integer currentGroup = (int) Math.ceil((double) currentPage / pageGroupSize);
Integer currentGroupFirstPage = (currentGroup - 1) * pageGroupSize + 1;
Integer currentGroupLastPage = Math.min(currentGroupFirstPage + pageGroupSize - 1, totalPages);
model.addAttribute("currentPagePosts", currentPagePosts);
model.addAttribute("currentPage", currentPage);
model.addAttribute("totalPages", totalPages);
model.addAttribute("currentGroupFirstPage", currentGroupFirstPage);
model.addAttribute("currentGroupLastPage", currentGroupLastPage);
model.addAttribute("pageGroupSize", pageGroupSize);
}
}
model 에 담아줘야 할 6가지 중 마지막으로
1. 현재 페이지에 보여질 게시글(PostsDTO)을 담은 List자료구조.
이거는 어떻게 구할까?
아래 부분이 그 기능을 수행한다.
Integer startRow = (currentPage - 1) * pageSize;
List<PostsDTO> currentPagePosts = new ArrayList<>();
// 사용자가 제목으로 조회한 경우.
if (byWhatType.equals("title")) { // hintToFind, startRow, pageSize
currentPagePosts = postsMapper.findPostsByTitle(hintToFind, startRow, pageSize);
}
// 사용자가 작성자로 조회한 경우.
if (byWhatType.equals("writer")) {
MemberJoinDTO findMember = memberMapper.findMemberById(hintToFind);
Integer id = findMember.getId();
currentPagePosts = postsMapper.findPostsByMemberId(id, startRow, pageSize);
}
// 사용자가 내용으로 조회한 경우.
if (byWhatType.equals("content")) {
currentPagePosts = postsMapper.findPostsByContent(hintToFind, startRow, pageSize);
}
startRow 는 "(현재페이지 - 1) * 페이지당 보여질 게시글의 수" 로 되어 있다.
이 startRow 가 뭐냐면, 현재 페이지에 보여질 게시글들을 데이터베이스에서 조회할 때 쓰인다.
예시를 통해 startRow의 기능을 설명해두자면, 다음과 같다.
예를 들어, 사용자가 "초코바"라는 제목으로 게시글을 조회하고자 하고 있다고 가정해보자.
이때, 초코바 라는 제목을 가진 게시글이 200개 있다고 해보자.
이 게시글들을 모두 한 페이지에 보여주는 게 아니라 "한 페이지당 보여질 게시글의 수(pageSize)" 만큼만 보여지게 할 것인데,
그러려면, 조회를 할 때, WHERE 절을 통해 제목이 "초코바" 인 게시글을 모두 조회한 다음에, 이 200개의 게시글들 중, 현재 페이지가 예를 들어 3페이지이고 한 페이지당 보여질 게시글의 수가 5개라면, 11번째 게시글부터 5개의 게시글만 조회해오면 되잖아.
이때, startRow 에 10 이 들어있어서, FETCH 10 ROWS 라고 쿼리로 하면, 조회된 게시글 중 11번째 글부터 가져오게 되는 것이다.
관련된 쿼리는 아래와 같다.
// PostsMapper 인터페이스
public List<PostsDTO> findPostsByTitle (String hintToFind, Integer startRow, Integer pageSize);
public List<PostsDTO> findPostsByMemberId (Integer memberId, Integer startRow, Integer pageSize);
public List<PostsDTO> findPostsByContent (String hintToFind, Integer startRow, Integer pageSize);
// PostsMapper.xml 파일
<select id="findPostsByTitle" resultType="PostsDTO">
SELECT posts.id, posts.member_id, posts.title, posts.is_private, TO_CHAR(posts.writing_date, 'YYYY-MM-DD') writing_date, posts.content, posts.delete_post_fl, post_view.view_number, member.user_id
FROM posts
LEFT OUTER JOIN post_view ON posts.id = post_view.post_id
JOIN member ON posts.member_id = member.id
WHERE posts.DELETE_POST_FL = 'N'
AND posts.title = #{hintToFind}
ORDER BY posts.id DESC
OFFSET #{startRow} ROWS
FETCH NEXT #{pageSize} ROWS ONLY
</select>
<select id="findPostsByMemberId" resultType="PostsDTO">
SELECT posts.id, posts.member_id, posts.title, posts.is_private, TO_CHAR(posts.writing_date, 'YYYY-MM-DD') writing_date, posts.content, posts.delete_post_fl, post_view.view_number, member.user_id
FROM posts
LEFT OUTER JOIN post_view ON posts.id = post_view.post_id
JOIN member ON posts.member_id = member.id
WHERE posts.DELETE_POST_FL = 'N'
AND posts.member_id = #{memberId}
ORDER BY posts.id DESC
OFFSET #{startRow} ROWS
FETCH NEXT #{pageSize} ROWS ONLY
</select>
<select id="findPostsByContent" resultType="PostsDTO">
SELECT posts.id, posts.member_id, posts.title, posts.is_private, TO_CHAR(posts.writing_date, 'YYYY-MM-DD') writing_date, posts.content, posts.delete_post_fl, post_view.view_number, member.user_id
FROM posts
LEFT OUTER JOIN post_view ON posts.id = post_view.post_id
JOIN member ON posts.member_id = member.id
WHERE posts.DELETE_POST_FL = 'N'
AND posts.content LIKE '%' || #{hintToFind} || '%'
ORDER BY posts.id DESC
OFFSET #{startRow} ROWS
FETCH NEXT #{pageSize} ROWS ONLY
</select>
그리고, FindPostsByTitleWriterService 클래스의 findPostsService 라는 메서드에는 아래 부분도 있었는데,
조회된 게시글이 없는 경우 "조회된 게시글이 없습니다" 라는 문구를 표시하기 위해서 nothing 이라는 key 로 request 영역에 데이터를 담아 두었고(model 도 결국 request 영역이니까),
// 만약 조회된 게시글이 하나도 없다면, "조회된 게시글이 없습니다" 라는 문구를 표시하기 위해서, model 에 담아줌.
if (currentPagePosts.size() == 0) {
model.addAttribute("nothing", "조회된 게시글이 없습니다.");
}
//boardHomeFind.jsp 파일에서 사용자가 입력한 byWhatType 과 hintToFind 를 그대로 보여주기 위해서 model 에 넣어둠
model.addAttribute("byWhatType", byWhatType);
model.addAttribute("hintToFind", hintToFind);
paginationService.pagination(currentPage, pageSize, totalPosts, pageGroupSize, currentPagePosts, model);
이제 이 모든 서비스계층을 거쳐, 아래의 컨트롤러의 마지막 부분인 아래 컨트롤러의 return 부분에 의해서 boardHomeFind.jsp 파일로 제어권이 이동할 것이다.
@Controller
@RequiredArgsConstructor
@Slf4j
public class FindPostsByTitleWriterContentController {
private final FindPostsByTitleWriterService findPostsByTitleWriterService;
@RequestMapping("find-posts-by-title-writer-content")
public String findWritingControllerMethod(@RequestParam String byWhatType, @RequestParam String hintToFind, @RequestParam(required = false, defaultValue = "1") Integer page, Model model) {
findPostsByTitleWriterService.findPostsService(byWhatType, hintToFind, page, model);
return "/contact/boardHomeFind";
}
}
boardHomeFind.jsp 파일의 전체코드는 아래 접어놨다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<c:import url="/jsp/bootstrapconfig/index.jsp"/>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %> <!-- 필요한 경우에 추가 -->
<%@ page import="firstportfolio.wordcharger.DTO.WritingDTOSelectVersion" %>
<html>
<head>
<link rel="stylesheet" href="../../css/board/boardHome.css">
</head>
<body>
<!--네브 바 -->
<c:import url="/jsp/common/loginedNavbar2.jsp" />
<!--네브 바 종료 -->
<div id="board-container">
<div id="page-title">
<div></div>
<div>게시판</div>
<div></div>
</div>
<div id="page-body">
<div></div>
<div>
<div></div>
<div>
<div class="board">
<span>글번호</span>
<span>제목</span>
<span>작성자</span>
<span>작성날짜</span>
<span>조회수</span>
</div>
</div>
<div>
<div class="board-2">
<c:forEach var="post" items="${currentPagePosts}">
<div class="post-row"> <!-- 각 게시물을 감싸는 div 추가 -->
<span>${post.id}</span>
<a href="/show-writing?postId=${post.id}">${post.title}</a>
<span>${post.userId}</span>
<span>${post.writingDate}</span>
<span>${post.viewNumber}</span>
</div>
</c:forEach>
<c:if test="${not empty nothing}">
<div style="display:flex; justify-content: center; align-items: center;"> ${nothing} </div>
</c:if>
</div>
<div id="post-make-div">
<a href="/writing-page" class="post-make" > 글 작성 </a>
<a href="/board-home" class="post-make" > 최신순 </a>
<a href="/board-home-order-by-like-num" class="post-make" > 좋아요순 </a>
<a href="/board-home-order-by-view-num" class="post-make" > 조회수순 </a>
</div>
<div id="find-posts">
<!--제목, 작성자, 내용 으로 게시글 찾기 -->
<form action="/find-posts-by-title-writer-content" method="post">
<select name="byWhatType">
<option value="title" ${byWhatType == 'title' ? 'selected' : ''} >제목</option>
<option value="writer" ${byWhatType == 'writer' ? 'selected' : ''} >작성자</option>
<option value="content" ${byWhatType == 'content' ? 'selected' : ''} >내용</option>
</select>
<input type="text" name="hintToFind" value="${hintToFind}" />
<button type="submit"> 찾기 </button>
</form>
</div>
<div id="pagination">
<!-- 페이지네이션 -->
<!-- 이전 그룹 링크 -->
<c:if test="${currentGroupFirstPage != 1}">
<a href="/find-posts-by-title-writer-content?page=${currentGroupFirstPage - pageGroupSize}&byWhatType=${byWhatType}&hintToFind=${hintToFind}">« 이전</a>
</c:if>
<!-- 현재 페이지 그룹의 페이지 링크 forEach 문 돌림 -->
<c:forEach var="i" begin="${currentGroupFirstPage}" end="${currentGroupLastPage}">
<c:choose>
<c:when test="${i == currentPage}">
<span class="current-page" style="color:#e06500; ">${i}</span>
</c:when>
<c:otherwise>
<a href="/find-posts-by-title-writer-content?page=${i}&byWhatType=${byWhatType}&hintToFind=${hintToFind}"">${i}</a>
</c:otherwise>
</c:choose>
</c:forEach>
<!-- 다음 그룹 링크 -->
<c:if test="${currentGroupLastPage != totalPages}">
<a href="/find-posts-by-title-writer-content?page=${currentGroupLastPage + 1}&byWhatType=${byWhatType}&hintToFind=${hintToFind}">다음 »</a>
</c:if>
</div>
</div>
</div>
<div>
</div>
</div>
</div>
</body>
</html>
다 볼 필요는 없고,
<c:if test="${not empty nothing}">
<div style="display:flex; justify-content: center; align-items: center;"> ${nothing} </div>
</c:if>
이 부분. 조회된 게시글이 없었을 경우에만 nothing 이라는 key 로 request 영역에 데이터를 저장해두었기 때문에, 조회된 게시글이 없다면, "조회된 게시글이 없습니다" 라는 문구가 뜨게 될 것이다.
그리고
<div id="pagination">
<!-- 페이지네이션 -->
<!-- 이전 그룹 링크 -->
<c:if test="${currentGroupFirstPage != 1}">
<a href="/find-posts-by-title-writer-content?page=${currentGroupFirstPage - pageGroupSize}&byWhatType=${byWhatType}&hintToFind=${hintToFind}">« 이전</a>
</c:if>
<!-- 현재 페이지 그룹의 페이지 링크 forEach 문 돌림 -->
<c:forEach var="i" begin="${currentGroupFirstPage}" end="${currentGroupLastPage}">
<c:choose>
<c:when test="${i == currentPage}">
<span class="current-page" style="color:#e06500; ">${i}</span>
</c:when>
<c:otherwise>
<a href="/find-posts-by-title-writer-content?page=${i}&byWhatType=${byWhatType}&hintToFind=${hintToFind}"">${i}</a>
</c:otherwise>
</c:choose>
</c:forEach>
<!-- 다음 그룹 링크 -->
<c:if test="${currentGroupLastPage != totalPages}">
<a href="/find-posts-by-title-writer-content?page=${currentGroupLastPage + 1}&byWhatType=${byWhatType}&hintToFind=${hintToFind}">다음 »</a>
</c:if>
</div>
페이지네이션 하는 이 부분을 보면, boardHome.jsp 파일과 비슷한데, a 태그의 href 부분이 좀 다르다.
boardHome.jsp 파일은 아래와 같았다.
아래 boardHome.jsp 파일과 다르게 위의 boardHomeFind.jsp 파일에서의 a태그는 byWhatType 과 hintToFind 를 쿼리스트링으로 넘긴다는 점. 그리고 get 요청하는 url 이 board-home 이 아니라, find-posts-by-title-writer-content 라는 점이 다르다.
<div id="pagination">
<!-- 페이지네이션 -->
<!-- 이전 그룹 링크 -->
<c:if test="${currentGroupFirstPage != 1}">
<a href="/board-home?page=${currentGroupFirstPage - pageGroupSize}">« 이전</a>
</c:if>
<!-- 현재 페이지 그룹의 페이지 링크 forEach 문 돌림 -->
<c:forEach var="i" begin="${currentGroupFirstPage}" end="${currentGroupLastPage}">
<c:choose>
<c:when test="${i == currentPage}">
<span class="current-page" style="color:#e06500; ">${i}</span>
</c:when>
<c:otherwise>
<a href="/board-home?page=${i}">${i}</a>
</c:otherwise>
</c:choose>
   
</c:forEach>
<!-- 다음 그룹 링크 -->
<c:if test="${currentGroupLastPage != totalPages}">
<a href="/board-home?page=${currentGroupLastPage + 1}">다음 »</a>
</c:if>
</div>
끝.
다음은 댓글에 관하여 포스팅할 것이다.