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
'💻 뚝딱뚝딱 > 팀내도서대여시스템(OBRS)' 카테고리의 다른 글
[개발일지#017] 로그인 인터셉터(Interceptor) 적용하기 (0) | 2025.02.20 |
---|---|
[개발일지#015] 대여내역 조회하기 (+ 썸네일 추가) (0) | 2025.02.20 |
[개발일지#014] 도서 API 적용하기 (Feat. 카카오 도서 API) (0) | 2025.02.20 |
[개발일지#013] 데이터베이스 컬럼추가 및 화면 수정 (0) | 2024.06.27 |
[개발일지#012] AWS(아마존 웹 서비스) 회원가입 하기 (0) | 2024.06.04 |
뚜루리님의
글이 좋았다면 응원을 보내주세요!
이 글이 도움이 됐다면, 응원 댓글을 써보세요. 블로거에게 지급되는 응원금은 새로운 창작의 큰 힘이 됩니다.
응원 댓글은 만 14세 이상 카카오계정 이용자라면 누구나 편하게 작성, 결제할 수 있습니다.
글 본문, 댓글 목록 등을 통해 응원한 팬과 응원 댓글, 응원금을 강조해 보여줍니다.
응원금은 앱에서는 인앱결제, 웹에서는 카카오페이 및 신용카드로 결제할 수 있습니다.