본문 바로가기
💻 뚝딱뚝딱/팀내도서대여시스템(OBRS)

[개발일지#016] 검색 기능 구현하기

by 뚜루리 2025. 2. 20.
728x90
320x100

 

[개발목표]

해당 메뉴 : 모든책 / 나의책 / 빌린책

  • 검색 기능 구현하기
  • 책 썸네일 이미지 보이게하기
  • 총 건수 보이게 하기
  • 게시판 넓이 변경 (더 넓게)

 

 


 

[구현화면]

모든책

  • 게시판 넓이를 넓혔음. (800 -> 1200으로...너무 좁아서!)
  • 책이름, 저자, 출판사명, 대여 가능한 책으로 검색 가능
  • 상단에 총건수를 표시.
  • 책 썸네일 이미지를 게시판에 포함.

 

나의책, 빌린책

  • 나의 책과 빌린책은 모든책과 검색기능이 동일하나 빌린책은 반납하지 않은 책만 볼 수 있도록 다른 검색조건을 설정하였음.

 


 

[구현하기]

  • 나의책과 빌린책은 거의 같은 방식으로 쿼리만 달라지기 때문에 모든책 화면을 기준으로 작성할 예정

bookMapper.xml

    <select id="searchBooks" resultType="seulgi.bookRentalSystem.domain.book.Book">
        SELECT
        BOOK_ID,
        BOOK_NAME,
        BOOK_WRITER,
        ISBN,
        PUBLISHER,
        THUMBNAIL_IMG,
        AUTHOR_ID,
        (SELECT MEMBER_NAME FROM MEMBER_TB WHERE MEMBER_ID = AUTHOR_ID) AS AUTHOR_NAME,
        BOOK_STATE_CODE,
        (SELECT STATE_CODE_NAME FROM BOOK_STATE_CODE WHERE STATE_CODE = BOOK_STATE_CODE) AS BOOK_STATE_CODE_NAME,
        CREATE_DATE
        FROM BOOK
        WHERE USE_AT = 'Y'
        <if test="category == 'all' and keyword != ''">
            AND (BOOK_NAME LIKE CONCAT('%', #{keyword}, '%')
            OR BOOK_WRITER LIKE CONCAT('%', #{keyword}, '%')
            OR PUBLISHER LIKE CONCAT('%', #{keyword}, '%'))
        </if>
        <if test="category == 'title'">
            AND BOOK_NAME LIKE CONCAT('%', #{keyword}, '%')
        </if>
        <if test="category == 'writer'">
            AND BOOK_WRITER LIKE CONCAT('%', #{keyword}, '%')
        </if>
        <if test="category == 'publisher'">
            AND PUBLISHER LIKE CONCAT('%', #{keyword}, '%')
        </if>
        <if test="onlyAvailable">
            AND BOOK_STATE_CODE = 'ABLE'
        </if>
        ORDER BY CREATE_DATE DESC
        LIMIT #{offset}, #{limit}
    </select>
  • if문 활용하여 검색조건에 맞춰 where문이 만들어 질수 있도록 구현

 

BookMapper.java

package seulgi.bookRentalSystem.domain.book;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface BookMapper {

 

    //모든 책 조회 (검색 조건)
    List<Book> searchBooks(@Param("category") String category,
                           @Param("keyword") String keyword,
                           @Param("onlyAvailable") boolean onlyAvailable,
                           @Param("offset") int offset,
                           @Param("limit") int limit);

    //모든 책 조회 (검색 조건) 건수
    int countSearchBooks(@Param("category") String category,
                         @Param("keyword") String keyword,
                         @Param("onlyAvailable") boolean onlyAvailable);



}

 

 

BookServiceImpl.java

package seulgi.bookRentalSystem.domain.book;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class BookServiceImpl implements BookService {

    private final BookMapper bookMapper;

    //모든 책 조회 (검색 조건)
    @Override
    public List<Book> searchBooks(String category, String keyword, boolean onlyAvailable, int page, int size) {
        int offset = (page - 1) * size;
        return bookMapper.searchBooks(category, keyword, onlyAvailable, offset, size);
    }

    //모든 책 조회 (검색 조건) 건수
    @Override
    public int countSearchBooks(String category, String keyword, boolean onlyAvailable) {
        return bookMapper.countSearchBooks(category, keyword, onlyAvailable);
    }


}

 

 

BookController.java

package seulgi.bookRentalSystem.web.book;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import seulgi.bookRentalSystem.domain.book.*;
import seulgi.bookRentalSystem.domain.member.Member;
import seulgi.bookRentalSystem.domain.member.MemberServiceImpl;
import seulgi.bookRentalSystem.domain.member.UpdateForm;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

@Controller
@RequiredArgsConstructor
@Transactional
@RequestMapping("/book")
public class BookController {

    private final BookServiceImpl bookService;

    /**
     * 책 전체 조회
     * @param model
     * @param request
     * @param page
     * @param size
     * @param category
     * @param keyword
     * @param onlyAvailable
     * @return
     */
    @GetMapping
    public String allBookList(Model model, HttpServletRequest request
        , @RequestParam(defaultValue = "1") int page
        , @RequestParam(defaultValue = "10") int size
        , @RequestParam(defaultValue = "all") String category // 📌 검색 기준 추가
        , @RequestParam(defaultValue = "") String keyword// 📌 검색어 추가
        , @RequestParam(defaultValue = "false") boolean onlyAvailable){
        String loginId = (String) request.getSession().getAttribute("loginId");
        List<Book> books = bookService.searchBooks(category, keyword, onlyAvailable, page, size);
        int totalBooks = bookService.countSearchBooks(category, keyword, onlyAvailable);

        int totalPages = (int) Math.ceil((double) totalBooks / size);

        model.addAttribute("loginId", loginId);
        model.addAttribute("books", books);
        model.addAttribute("totalBooks", totalBooks);
        model.addAttribute("currentPage", page);
        model.addAttribute("totalPages", totalPages);
        model.addAttribute("category", category);
        model.addAttribute("keyword", keyword);
        model.addAttribute("onlyAvailable", onlyAvailable);
        return "book/allBookList";
    }

 
}

 

 

allBookList.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{common/layout}"
      layout:fragment="content">
<head>
    <title>모든 책</title>
    <script th:src="@{/js/book/allBookList.js}"></script>
</head>
<body>
<div class="container" id="content" style="max-width: 1200px;">
    <div class="py-5 text-center">
        <h2>모든 책</h2>
    </div>

    <!-- 📌 검색 필터 추가 -->
    <div class="row">
        <div class="col-md-4">
            <select id="searchCategory" class="form-select">
                <option value="all" th:selected="${category == 'all'}">전체</option>
                <option value="title" th:selected="${category == 'title'}">책 제목</option>
                <option value="writer" th:selected="${category == 'writer'}">저자</option>
                <option value="publisher" th:selected="${category == 'publisher'}">출판사</option>
            </select>
        </div>
        <div class="col-md-6">
            <input type="text" id="searchKeyword" class="form-control" placeholder="검색어를 입력하세요">
        </div>
        <div class="col-md-2">
            <button class="btn btn-primary w-100" id="searchBookBtn">검색</button>
        </div>
    </div>

    <!-- 📌 대여 가능 책만 보기 체크박스 추가 -->
    <!-- 대여 가능 체크박스 -->
    <div class="form-check mt-3">
        <input type="checkbox" class="form-check-input" id="onlyAvailableBooks" th:checked="${onlyAvailable}">
        <label class="form-check-label" for="onlyAvailableBooks">대여 가능 책만 보기</label>
    </div>

    <div class="row">
        <div class="col">
            <button class="btn btn-primary float-end" type="button" id="addBookBtn">책 등록</button> </div>
    </div>
    <hr class="my-4">
    <span th:text="${loginId}" id="loginId" style="display: none;"></span>
    <div>
        <h5><span th:text="${totalBooks}">0</span></h5>
        <table class="table">
            <thead>
            <tr style="text-align: center;">
                <th>No</th>
                <th>이미지</th>
                <th>책이름</th>
                <th>저자</th>
                <th>소유자</th>
                <th>상태</th>
                <th>등록일</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="book, stat : ${books}">
                <td><a th:text="${stat.count}" style="text-align: center;">1</a></td>
                <td style="text-align: center;">
                    <img id="bookImage" th:if="${book.thumbnailImg != null and book.thumbnailImg != ''}"
                         th:src="${book.thumbnailImg}" alt="책 이미지"
                         style="width: 50px; height: auto; border-radius: 10px;">
                </td>
                <td><a href="book.html" th:href="@{/book/{bookId} (bookId=${book.bookId})}" th:text="${book.bookName}">책 이름</a></td>
                <td><a th:text="${book.bookWriter}">저자</a></td>
                <td style="text-align: center;"><a th:text="${book.authorName}">소유자</a></td>
                <td style="text-align: center;">
                    <a th:if="${book.bookStateCode == 'ABLE'}" th:text="${book.bookStateCodeName}" style="color: darkgreen;">상태</a>
                    <a th:if="${book.bookStateCode == 'UNABLE'}" th:text="${book.bookStateCodeName}" style="color: red;">상태</a>
                </td>
                <td style="text-align: center;"><a th:text="${book.createDate}">등록일</a></td>
            </tr>
            </tbody>
        </table>
    </div>
    <th:block th:include="common/bookPagenation :: pagenation"></th:block>
    <hr class="my-4">
    </div> <!-- /container -->
</body>
</html>

 

 

 

allBookList.js

function addBookBtn() {
    let loginId = document.getElementById("loginId").textContent;
    if (loginId === ''){
        alert("로그인이 필요한 서비스입니다.");
        window.location.href = "/login";
    } else {
        window.location.href = "/book/addBook";
    }
}

function searchBooks() {
    let category = document.getElementById("searchCategory").value;
    let keyword = document.getElementById("searchKeyword").value.trim();
    let onlyAvailable = document.getElementById("onlyAvailableBooks").checked;

    let url = `/book?category=${category}&keyword=${encodeURIComponent(keyword)}&onlyAvailable=${onlyAvailable}`;
    window.location.href = url;
}

document.addEventListener('DOMContentLoaded', function (){
    document.getElementById("addBookBtn").addEventListener('click', addBookBtn);

    // 📌 검색 버튼 클릭 시 검색 실행
    document.getElementById("searchBookBtn").addEventListener("click", searchBooks);

    // 📌 Enter 키 입력 시 검색 실행
    document.getElementById("searchKeyword").addEventListener("keypress", function (event) {
        if (event.key === "Enter") {
            searchBooks();
        }
    });

    // 📌 대여 가능 책만 보기 체크박스 이벤트
    document.getElementById("onlyAvailableBooks").addEventListener("change", searchBooks);

// 📌 검색 후에도 입력 값과 체크박스 유지 (URL에서 값 읽어오기)
    const params = new URLSearchParams(window.location.search);
    if (params.has("category")) {
        document.getElementById("searchCategory").value = params.get("category");
    }
    if (params.has("keyword")) {
        document.getElementById("searchKeyword").value = params.get("keyword");
    }
    if (params.has("onlyAvailable")) {
        document.getElementById("onlyAvailableBooks").checked = params.get("onlyAvailable") === "true";
    }

});
728x90
320x100

뚜루리님의
글이 좋았다면 응원을 보내주세요!