카테고리 없음

게시판 기능 구현 6 : 조회조건 - 스프링, jsp, 오라클, mybatis

blogOwner 2024. 3. 26. 22:45
더보기

게시판 홈에서 아래와 같이 좋아요순으로 게시글을 조회하기, 조회수순으로 게시글을 조회하기 두 기능을 구현해볼 것이다.

 

그리고, 게시판 홈에서, 아래와 같이, 제목 또는 작성자 또는 내용으로 게시물을 조회할 수 있도록 해두었는데, 이를 구현해볼 것이다. 

 

 

----------------------------------------------------------------------------------------------------------------------

 

우선, 간단한 조회수순 조회, 좋아요순 조회를 구현해보자.

일단, 이전에 만들었던 것이 최신순으로 게시글을 조회하는 거였거든? 

이 부분을 다시 복기해보자.

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" >&nbsp;최신순&nbsp;</a>
<a href="/board-home-order-by-like-num" class="post-make" >&nbsp;좋아요순&nbsp;</a>
<a href="/board-home-order-by-view-num" class="post-make" >&nbsp;조회수순&nbsp;</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" >&nbsp;글 작성&nbsp;</a>
                            <a href="/board-home" class="post-make" >&nbsp;최신순&nbsp;</a>
                            <a href="/board-home-order-by-like-num" class="post-make" >&nbsp;좋아요순&nbsp;</a>
                            <a href="/board-home-order-by-view-num" class="post-make" >&nbsp;조회수순&nbsp;</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}">&laquo; 이전</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}">다음 &raquo;</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}">&laquo; 이전</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}">다음 &raquo;</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}">&laquo; 이전</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>
    &nbsp &nbsp
</c:forEach>

<!-- 다음 그룹 링크 -->
<c:if test="${currentGroupLastPage != totalPages}">
    <a href="/board-home?page=${currentGroupLastPage + 1}">다음 &raquo;</a>
</c:if>
</div>

 

끝. 

 

다음은 댓글에 관하여 포스팅할 것이다.