Coding History

국비 지원 IT(웹앱개발) 취업반 강의 52일차 (Spring)

BlackBirdIT 2024. 8. 21. 10:05

어제 페이지네이션을 이어서 해보자. 데이터를 극단적으로 늘리니까 문제가 하나 보였는데,

페이지가 많아짐에 따라서 밑의 숫자도 많아져버린다는 문제점이 보인다.

그래서 일단은 이렇게 처리를 했다.

<div class="pagination">
    <c:if test="${currentPage > 1}">
        <a href="?boardId=${boardId}&page=${currentPage - 1}" class="btn">Previous</a>
    </c:if>

    <c:choose>
        <c:when test="${currentPage == 1}">
            <span class="current">1</span>
        </c:when>
        <c:otherwise>
            <a href="?boardId=${boardId}&page=1" class="btn">1</a>
        </c:otherwise>
    </c:choose>

    <c:choose>
        <c:when test="${currentPage > 3}">
            <span class="dots" onclick="showPageInput(this)">...</span>
            <div class="page-input hidden">
                <input type="number" class="pageInputField" min="1" max="${totalPages}" placeholder="Page number">
                <button onclick="goToPage(this, ${boardId})">Go</button>
            </div>
        </c:when>
    </c:choose>

    <c:forEach var="i" begin="${currentPage > 3 ? currentPage - 2 : 2}" end="${currentPage + 2 > totalPages ? totalPages - 1 : currentPage + 2}">
        <c:choose>
            <c:when test="${i == currentPage}">
                <span class="current">${i}</span>
            </c:when>
            <c:otherwise>
                <a href="?boardId=${boardId}&page=${i}" class="btn">${i}</a>
            </c:otherwise>
        </c:choose>
    </c:forEach>

    <c:choose>
        <c:when test="${currentPage < totalPages - 2}">
            <span class="dots" onclick="showPageInput(this)">...</span>
            <div class="page-input hidden">
                <input type="number" class="pageInputField" min="1" max="${totalPages}" placeholder="Page number">
                <button onclick="goToPage(this, ${boardId})">Go</button>
            </div>
        </c:when>
    </c:choose>

    <c:choose>
        <c:when test="${currentPage == totalPages}">
            <span class="current">${totalPages}</span>
        </c:when>
        <c:otherwise>
            <a href="?boardId=${boardId}&page=${totalPages}" class="btn">${totalPages}</a>
        </c:otherwise>
    </c:choose>

    <c:if test="${currentPage < totalPages}">
        <a href="?boardId=${boardId}&page=${currentPage + 1}" class="btn">Next</a>
    </c:if>
</div>

조건문을 붙혀서 화면에 보이는 것을 처리했고 눈으로 보이는 것은 이렇게 보인다.

...을 클릭해서 페이지로 바로 이동할 수 있게 입력 받은 정수를 url로 전송하고 해당 페이지로 이동하는 것을 만들어보려고 했는데 생각처럼 잘 안돼서 일단은 남겨놨다.

검색기능을 만들었는데.

<div class="search-bar">
    <form action="/usr/article/list" method="GET">
        <div class="search-container">
            <!-- 첫 번째 칸: 제목, 내용, 작성자 선택 -->
            <select name="searchField" class="search-field-select">
                <option value="title">제목</option>
                <option value="body">내용</option>
                <option value="extra__writer">작성자</option>
            </select>

            <!-- 두 번째 칸: 사용자 입력 -->
            <input type="text" name="searchKeyword" class="search-input" placeholder="검색어를 입력하세요">

            <!-- 검색 버튼 -->
            <button type="submit" class="search-button">검색</button>
        </div>

        <!-- Hidden field for boardId to keep the context of the board -->
        <input type="hidden" name="boardId" value="${boardId}">

        <!-- Hidden field for the current page to keep pagination context -->
        <input type="hidden" name="page" value="${currentPage}">
    </form>
</div>

화면구성은 이렇게 했고,

    @RequestMapping("/usr/article/list")
    public String showList(Model model, Integer page, @RequestParam(defaultValue = "1") int boardId,
            @RequestParam(value = "searchField", required = false) String searchField,
            @RequestParam(value = "searchKeyword", required = false) String searchKeyword) {

        if (page == null) {
            page = 1; // 기본값 설정
        }
        Board board = boardService.getBoardById(boardId);
        model.addAttribute("board", board);

        int itemsPerPage = 10;
        int totalItems = articleService.getTotalArticlesCount(boardId);
        int totalPages = (int) Math.ceil((double) totalItems / itemsPerPage);
        int offset = (page - 1) * itemsPerPage;

        List<Article> articles = articleService.getArticlesByPage(boardId, offset, itemsPerPage);

        // 검색어와 검색 조건이 있는 경우 필터링된 게시물 가져오기
        if (searchKeyword != null && !searchKeyword.trim().isEmpty()) {
            totalItems = articleService.getTotalArticlesCountBySearch(boardId, searchField, searchKeyword);
            articles = articleService.getArticlesByPageAndSearch(boardId, searchField, searchKeyword,
                    (page - 1) * itemsPerPage, itemsPerPage);
        } else {
            totalItems = articleService.getTotalArticlesCount(boardId);
            articles = articleService.getArticlesByPage(boardId, (page - 1) * itemsPerPage, itemsPerPage);
        }

        model.addAttribute("articles", articles);
        model.addAttribute("currentPage", page);
        model.addAttribute("totalPages", totalPages);
        model.addAttribute("boardId", boardId);
        //검색어 통신
        model.addAttribute("searchField", searchField);
        model.addAttribute("searchKeyword", searchKeyword);

        return "usr/article/list";
    }
    //검색기능.
    public int getTotalArticlesCountBySearch(int boardId, String searchField, String searchKeyword) {
        return articleRepository.getTotalArticlesCountBySearch(boardId, searchField, searchKeyword);
    }

    public List<Article> getArticlesByPageAndSearch(int boardId, String searchField, String searchKeyword, int offset, int limit) {
        return articleRepository.getArticlesByPageAndSearch(boardId, searchField, searchKeyword, offset, limit);
    }
    //검색기능 
//    @Select("""
//            SELECT COUNT(*)
//            FROM article
//            WHERE boardId = #{boardId}
//            AND ${searchField} LIKE CONCAT('%', #{searchKeyword}, '%')
//        """)
    public int getTotalArticlesCountBySearch(int boardId, String searchField, String searchKeyword);

//    @Select("""
//            SELECT *
//            FROM article
//            WHERE boardId = #{boardId}
//            AND ${searchField} LIKE CONCAT('%', #{searchKeyword}, '%')
//            ORDER BY id DESC
//            LIMIT #{limit} OFFSET #{offset}
//        """)
    public List<Article> getArticlesByPageAndSearch(int boardId, String searchField, String searchKeyword, int limit,
            int offset);

    <!-- 검색기능. -->
    <select id="getTotalArticlesCountBySearch" resultType="int">
        SELECT COUNT(*)
        FROM article
        WHERE boardId = #{boardId}
         <if test="searchField != null and searchKeyword != null">
               AND ${searchField} LIKE CONCAT('%', #{searchKeyword}, '%')
        </if>
    </select>

    <select id="getArticlesByPageAndSearch" resultType="com.example.demo.vo.Article">
        SELECT *
        FROM article
        WHERE boardId = #{boardId}
        <if test="searchField != null and searchKeyword != null">
            AND ${searchField} LIKE CONCAT('%', #{searchKeyword}, '%')
        </if>
        ORDER BY id DESC
        LIMIT #{limit} OFFSET #{offset}
    </select>

이렇게 처리했다.

화면상으로는 이렇게 보이고,

제목에 45를 검색했을 때,

내용에 45를 검색했을 때, (내용 두개 까보니까 45가 있다.)

작성자는 검색시 오류가 난다. 애초에 저기 보면 작성자를 불러오지 못하고 있다. resultMap이 필요할 것 같다.

resulMap을 수정해서 작성자를 가져와서 다시 해봤는데 bad SQL이라고 오류가 났다. 그래서 보니까 AS때문인 것 같아서 쿼리를 다시 수정했다.

    <!-- 검색기능. -->
    <select id="getTotalArticlesCountBySearch" resultType="int">
        SELECT COUNT(*)
        FROM article a
        INNER JOIN `member` m ON a.memberId =
        m.id
        WHERE a.boardId = #{boardId}
        <if test="searchField != null and searchKeyword != null">
            <choose>
                <when test="searchField == 'extra__writer'">
                    AND m.nickname LIKE CONCAT('%', #{searchKeyword},
                    '%')
                </when>
                <otherwise>
                    AND ${searchField} LIKE CONCAT('%', #{searchKeyword}, '%')
                </otherwise>
            </choose>
        </if>
    </select>

    <select id="getArticlesByPageAndSearch"
        resultMap="ArticleResultMap">
        SELECT a.*, m.nickname AS extra__writer
        FROM article a
        INNER JOIN `member` m ON a.memberId = m.id
        WHERE a.boardId = #{boardId}
        <if test="searchField != null and searchKeyword != null">
            <choose>
                <when test="searchField == 'extra__writer'">
                    AND m.nickname LIKE CONCAT('%', #{searchKeyword}, '%')
                </when>
                <otherwise>
                    AND ${searchField} LIKE CONCAT('%', #{searchKeyword}, '%')
                </otherwise>
            </choose>
        </if>
        ORDER BY a.id DESC
        LIMIT #{limit} OFFSET #{offset}
    </select>

작성자 탭에서 길동이라고 검색해서 얻은 결과다.

이제 검색기능은 어떻게 다 잘 되는데 이게 또 문제가 검색을 하고 난 뒤에 페이징을 어떻게 하느냐가 문제다.

그래서 GPT씨 한테 물어봤다

1. 기존 페이징 시스템 재사용:

검색 조건에 따라 필터링된 데이터에 대해서도 페이징 처리를 동일하게 수행합니다.
검색 쿼리와 함께 페이지 번호를 유지하고, 검색 결과에 따라 페이지 링크를 생성하여 사용자가 검색 결과의 다른 페이지로 이동할 수 있도록 합니다.

2. 검색 조건 유지:

페이지네이션 링크를 클릭할 때 검색 조건(searchField와 searchKeyword)이 계속 URL에 포함되어야 합니다.
이를 위해 페이징 링크에 검색 파라미터를 추가하여 검색 결과에 대한 페이징을 유지합니다.

라고 한다. 그럼 기존 페이징 시스템을 고쳐야되는거고 안그래도 검색 후에 다른 페이지를 보려고 클릭하니까 기존 목록으로 돌아가는게 거슬렸는데 그것 까지 함께 처리해보자.

아 지금 4시간 째 하고 있는데 어떻게 고치는지 모르겠다. 일단 포기다.

<div class="search-bar">
    <form action="/usr/article/list" method="GET">
        <div class="search-container">
            <select name="searchField" class="search-field-select" data-value="${param.searchField}">
                <option value="title">제목</option>
                <option value="body">내용</option>
                <option value="extra__writer">작성자</option>
            </select> <input type="text" name="searchKeyword" class="search-input"
                placeholder="검색어를 입력하세요" value="${searchKeyword}">

            <button type="submit" class="search-button">검색</button>
        </div>

        <input type="hidden" name="boardId" value="${boardId}"> <input
            type="hidden" name="page" value="1">
    </form>
</div>

와중에 검색 후 초기화 되는게 거슬려서 데이터가 남게끔 처리했다. searchField는 JS로 처리했는데 data-value="${param.searchField}이렇게 남겨주고

$('select[data-value]').each(function(index, el) {
    const $el = $(el);

    defaultValue = $el.attr('data-value').trim();

    if (defaultValue.length > 0) {
        $el.val(defaultValue);
    }
});

해당 JS코드를 추가해서 검색후에도 사용자가 선택한 탭과 검색어가 남아있도록 만들었다.

와 드디어 고쳤다.

        if (page == null || (searchField != null && searchKeyword != null)) {
            page = 1; // 검색 시 첫 페이지로 설정
        }

아까 검색하고 난 다음에 3페이지에서 만약 검색을 시도하면 3페이지에서 시작하는게 거슬려서 달아뒀던 조건문이다.

근데 이게 문제였다. 내가 페이지를 옮기려고 해도 검색은 유지된 채로 옮기기 때문에 1로 계속 초기화 시켜서 1페이지로 고정됐던 것.

        if (page == null) {
            page = 1; // 기본값 설정
        }

이렇게 고쳤다. 아니 아예 빼도 될 것 같다.

어우 진짜 속시원하네...

아무튼 이제 조회수 처리 해보자.

조회수는 디테일 페이지에 들어갔을 때 카운트되어야하기 때문에 detail메서드에서 구현할 기능을 추가하면 된다.

    @RequestMapping("/usr/article/detail")
    public String showDetail(HttpServletRequest req, Model model, int id) {

        Rq rq = (Rq) req.getAttribute("rq");

        articleService.increaseHitCount(id);

        Article article = articleService.getForPrintArticle(rq.getLoginedMemberId(), id);

        model.addAttribute("article", article);

        return "usr/article/detail";

articleService.increaseHitCount(id); 라는 메서드를 하나 만들어줬다. 마찬가지로 여태 했던 것 처럼 service와 Repository에도 생성했고, 쿼리문을 한번 보자.

    @Update("""
            UPDATE article
            SET hitCount = hitCount + 1
            WHERE id = #{id}
            """)
    public void increaseHitCount(int id);    

이게 쿼리인데 보면 없는 테이블이다. 그래서 DB에서 테이블 추가를 해주자.

ALTER TABLE article ADD COLUMN hitCount INT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER boardId;

이렇게.

    <div class="detail-item">
        <span class="label">조회수:</span> ${article.hitCount}
    </div>

디테일 JSP에 조회수까지 추가해주고 hitCount라는 매개변수를 가져올 수 있게 Article 에 선언해주면 끝.

새로고침하면 조회수가 계속 오르는 것을 볼 수 있다!

오늘은 여기까지.