qs 라이브러리 설치
npm i qs
Main.js (수정)
qs 라이브러리 적용
const categoryClickHandle = (e) => {
if (e.target.tagName === "INPUT" || e.target.tagName === "LABEL") {
return;
}
e.stopPropagation();
setIsOpen(!isOpen);
};
const categoryCheckHandle = (e) => {
if(e.target.checked){
setSearchParam({...searchParam, page: 1, categoryIds: [...searchParam.categoryIds, e.target.value]});
}else{
setSearchParam({...searchParam, page: 1, categoryIds: [...searchParam.categoryIds.filter(id => id!== e.target.value)]});
}
setBooks([]);
setRefresh(true);
}
return (
<div css ={mainContainer}>
<Sidebar></Sidebar>
<header css={header}>
<div css ={title}>도서검색</div>
<div css ={searchItems}>
<button css={categoryButton} onClick={categoryClickHandle} >
<BsMenuDown />
<div css={categoryGroup(isOpen)}>
{categories.data !== undefined
? categories.data.data.map(category =>
(<div key={category.categoryId} className="my-class">
<input type="checkbox" onChange={categoryCheckHandle} id={"ct-" + category.categoryId} value={category.categoryId} />
<label htmlFor={"ct-" + category.categoryId}>{category.categoryName}</label>
</div>))
:""}
</div>
</button>
<input css={searchInput} type="search" />
</div>
</header>
<main css ={main}>
{books.length > 0 ? books.map(book => (<BookCard key={book.bookId} book={book}></BookCard>)) : ""}
<div ref={lastBookRef}></div>
</main>
</div>
);
BookMapper.xml (추가)
where 조건 추가
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.toyproject.bookmanagement.repository.BookRepository">
<resultMap type="com.toyproject.bookmanagement.entity.Book" id="BookMap">
<id property="bookId" column="book_id"/>
<result property="bookName" column="book_name"/>
<result property="authorId" column="author_id"/>
<result property="publisherId" column="publisher_id"/>
<result property="categoryId" column="category_id"/>
<result property="coverImgUrl" column="cover_img_url"/>
<association property="author" resultMap="AuthorMap"></association>
<association property="publisher" resultMap="PublisherMap"></association>
<association property="category" resultMap="CategoryMap"></association>
</resultMap>
<resultMap type="com.toyproject.bookmanagement.entity.Author" id="AuthorMap">
<id property="authorId" column="author_id"/>
<result property="authorName" column="author_name"/>
</resultMap>
<resultMap type="com.toyproject.bookmanagement.entity.Publisher" id="PublisherMap">
<id property="publisherId" column="publisher_id"/>
<result property="publisherName" column="publisher_name"/>
</resultMap>
<resultMap type="com.toyproject.bookmanagement.entity.Category" id="CategoryMap">
<id property="categoryId" column="category_id"/>
<result property="categoryName" column="category_name"/>
</resultMap>
<select id="searchBooks" parameterType="hashMap" resultMap="BookMap">
select
bt.book_id,
bt.book_name,
bt.author_id,
bt.publisher_id,
bt.category_id,
bt.cover_img_url,
at.author_id,
at.author_name,
pt.publisher_id,
pt.publisher_name,
ct.category_id,
ct.category_name
from
book_tb bt
left outer join author_tb at on (at.author_id = bt.author_id)
left outer join publisher_tb pt on (pt.publisher_id = bt.publisher_id)
left outer join category_tb ct on (ct.category_id = bt.category_id)
where
1= 1
<if test="categoryIds != null">
and bt.category_id in (
<foreach item="categoryId" collection="categoryIds" separator=",">
#{categoryId}
</foreach>
)
</if>
order by
bt.book_id
limit #{index}, 20;
</select>
<select id="getTotalCount" parameterType="hashMap" resultType="Integer">
select
count(*)
from
book_tb bt
left outer join author_tb at on (at.author_id = bt.author_id)
left outer join publisher_tb pt on (pt.publisher_id = bt.publisher_id)
left outer join category_tb ct on (ct.category_id = bt.category_id)
where
1= 1
<if test="categoryIds != null">
and bt.category_id in (
<foreach item="categoryId" collection="categoryIds" separator=",">
#{categoryId}
</foreach>
)
</if>
</select>
<select id="getCategories" resultMap="CategoryMap">
select
category_id,
category_name
from
category_tb
</select>
</mapper>
카테고리 여행 선택 시
Main.js (추가 & 수정)
searchBooks 함수
const searchInputHandle = (e) => {
setSearchParam({...searchParam, searchValue: e.target.value});
}
const searchSubmitHandle = (e) => {
if(e.keyCode === 13){
setSearchParam({...searchParam, page: 1});
setBooks([]);
setRefresh(true);
}
}
BookService (추가)
public Map<String, Object> searchBooks(SearchBookReqDto searchBookReqDto){
List<SearchBookRespDto> list = new ArrayList<>();
int index = (searchBookReqDto.getPage() - 1) * 20; // 20 부분 수정 가능
Map<String, Object> map = new HashMap<>();
map.put("index" , index);
map.put("categoryIds", searchBookReqDto.getCategoryIds());
map.put("searchValue", searchBookReqDto.getSearchValue());
bookRepository.searchBooks(map).forEach(book -> {
list.add(book.toDto());
});
int totalCount = bookRepository.getTotalCount(map);
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("totalCount", totalCount);
responseMap.put("bookList", list);
return responseMap;
}
BookMapper.xml (추가)
<select id="searchBooks" parameterType="hashMap" resultMap="BookMap">
select
bt.book_id,
bt.book_name,
bt.author_id,
bt.publisher_id,
bt.category_id,
bt.cover_img_url,
at.author_id,
at.author_name,
pt.publisher_id,
pt.publisher_name,
ct.category_id,
ct.category_name
from
book_tb bt
left outer join author_tb at on (at.author_id = bt.author_id)
left outer join publisher_tb pt on (pt.publisher_id = bt.publisher_id)
left outer join category_tb ct on (ct.category_id = bt.category_id)
where
1= 1
<if test="categoryIds != null">
and bt.category_id in (
<foreach item="categoryId" collection="categoryIds" separator=",">
#{categoryId}
</foreach>
)
</if>
and bt.book_name like concat("%",#{searchValue}, "%")
order by
bt.book_id
limit #{index}, 20;
</select>
<select id="getTotalCount" parameterType="hashMap" resultType="Integer">
select
count(*)
from
book_tb bt
left outer join author_tb at on (at.author_id = bt.author_id)
left outer join publisher_tb pt on (pt.publisher_id = bt.publisher_id)
left outer join category_tb ct on (ct.category_id = bt.category_id)
where
1= 1
<if test="categoryIds != null">
and bt.category_id in (
<foreach item="categoryId" collection="categoryIds" separator=",">
#{categoryId}
</foreach>
)
</if>
and bt.book_name like concat("%",#{searchValue}, "%")
</select>
BookCard.js (수정)
const cardContainer = css`
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
margin: 20px;
border: 1px solid #dbdbdb;
border-radius: 7px;
box-shadow: 0px 0px 5px #dbdbdb;
width: 300px;
max-height: 450;
cursor: pointer;
&:hover {
box-shadow: 0px 0px 10px #dbdbdb;
}
&:active {
background-color: #fafafa;
}
`;
App.js (추가)
pages > BookDetail
BookDetail.js (생성)
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React from 'react';
import Sidebar from '../../components/Sidebar/Sidebar';
import { useParams } from 'react-router-dom';
import { useQuery } from 'react-query';
import axios from 'axios';
const mainContainer = css`
padding: 10px;
`;
const BookDetail = () => {
// App.js 의 path="/book/:bookId"
const { bookId } = useParams();
const getBook = useQuery(["getBook"], async() => {
const option = {
headers: {
Authorization: localStorage.getItem("accessToken")
}
}
const response = await axios.get(`http://localhost:8080/book/${bookId}`, option);
return response;
});
if(getBook.isLoading) {
return <div>불러오는 중...</div>
}
if(!getBook.isLoading)
return (
<div css={mainContainer}>
<Sidebar />
<header>
<h1>{getBook.data.data.bookName}</h1>
<p>분류: {getBook.data.data.categoryName} / 저자명: {getBook.data.data.authorName}/ 출판사: {getBook.data.data.publisherName} / 추천: 10 </p>
</header>
<main>
<div>
<img src={getBook.data.data.coverImgUrl} alt={getBook.data.data.categoryName} />
</div>
<div>
</div>
<div>
</div>
</main>
</div>
);
};
export default BookDetail;
특징 | 설명 |
---|---|
데이터 패칭 | 원격 API로부터 데이터를 가져오는 과정을 캡슐화합니다. |
캐싱 | 가져온 데이터를 자동으로 캐싱하여 중복 요청을 방지하고 성능과 사용자 경험을 향상시킵니다. |
자동 리패칭 | 일정 시간 간격으로 데이터를 자동으로 리패칭하여 최신 상태로 유지할 수 있습니다. |
상태 관리 | isLoading, isError, data, error 등의 상태를 쉽게 추적하여 UI를 쉽게 업데이트할 수 있습니다. |
쿼리 취소 | 요청이 더 이상 필요하지 않은 경우 자동으로 취소하여 불필요한 네트워크 트래픽을 방지합니다. |
페이징 및 무한 스크롤 | useInfiniteQuery와 같은 추가 훅을 사용하여 페이징 또는 무한 스크롤 기능을 쉽게 구현할 수 있습니다. |
const getUserData = (userId, callback) => {
setTimeout(() => {
console.log("데이터를 불러오는 중...");
const userData = {
id: userId,
name: "John Doe",
};
callback(userData);
}, 1000);
};
console.log("데이터 요청 시작");
getUserData(1, (userData) => {
console.log("데이터 요청 완료");
console.log(userData);
});
console.log("다른 작업 수행");
console.log("데이터 요청 시작");
실행getUserData
함수호출,userId
인1
과 콜백 함수를 인수로 전달getUserData
함수 내에서setTimeout()
이 호출되어,1000ms(1초)
후에 콜백함수를 실행하도록 예약, 이 시간 동안, 코드는 다른 작업을 계속 수행console.log("다른 작업 수행")
을 실행- 1초가 지나면,
setTimeout()
에 전달된 화살표 함수가 실행, 이 함수는데이터를 불러오는 중...
을 출력하고,userData
객체를 생성한 다음,callback(userData)
를 호출하여 초기에 전달한 콜백 함수를 실행- 콜백 함수가 실행되면,
console.log("데이터 요청 완료")
를 호출하여 출력하고,console.log(userData)
를 호출하여userData
객체를 출력
BookCard.js (이벤트 추가)
const BookCard = ({ book }) => {
const navigate = useNavigate();
const clickHandle = () => {
navigate("/book/" + book.bookId);
}
return (
<div css={cardContainer} onClick={clickHandle}>
<header css={header}>
<h1 css={titleText}>{book.bookName} </h1>
</header>
<main css={main}>
<div css ={imgBox}>
<img css={img} src={book.coverImgUrl} alt={book.bookName} />
</div>
</main>
<footer css={footer}>
<div css={like}><div css={likeIcon}><AiOutlineLike /></div>추천: 10 </div>
<h2>저자명: {book.authorName}</h2>
<h2>출판사: {book.publisherName}</h2>
</footer>
</div>
);
};
BookController (getBooks 메서드 추가)
@GetMapping("/book/{bookId}")
public ResponseEntity<?> getBooks(@PathVariable int bookId){
return ResponseEntity.ok().body(bookService.getBook(bookId));
}
BookRepository (getBook 메서드 추가)
package com.toyproject.bookmanagement.repository;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import com.toyproject.bookmanagement.entity.Book;
import com.toyproject.bookmanagement.entity.Category;
@Mapper
public interface BookRepository {
public Book getBook(int bookId);
public List<Book> searchBooks(Map<String, Object> map);
public int getTotalCount(Map<String, Object> map);
public List<Category> getCategories();
}
BookMapper.xml ( getBook select문 추가)
<select id="getBook" parameterType="Interger" resultMap="BookMap">
select
bt.book_id,
bt.book_name,
bt.author_id,
bt.publisher_id,
bt.category_id,
bt.cover_img_url,
at.author_id,
at.author_name,
pt.publisher_id,
pt.publisher_name,
ct.category_id,
ct.category_name
from
book_tb bt
left outer join author_tb at on (at.author_id = bt.author_id)
left outer join publisher_tb pt on (pt.publisher_id = bt.publisher_id)
left outer join category_tb ct on (ct.category_id = bt.category_id)
where
bt.book_id = #{bookId}
</select>
BookService (GetBookRespDto을 타입으로 하는 getBook 메서드 생성)
public GetBookRespDto getBook(int bookId) {
return bookRepository.getBook(bookId).toGetBookDto();
}
dto > book
GetBookRespDto (생성)
package com.toyproject.bookmanagement.dto.book;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
public class GetBookRespDto {
private int bookId;
private String bookName;
private String authorName;
private String publisherName;
private String categoryName;
private String coverImgUrl;
}
entity > Book (GetBookRespDto 메서드 추가)
public GetBookRespDto toGetBookDto() {
return GetBookRespDto.builder()
.bookId(bookId)
.bookName(bookName)
.authorName(author.getAuthorName())
.publisherName(publisher.getPublisherName())
.categoryName(category.getCategoryName())
.coverImgUrl(coverImgUrl)
.build();
}
Dto
만을 사용할 것을 장려book_managemnet
book_like_tb 생성
BookRepository ( getLikeCount 메서드 추가)
public int getLikeCount(int bookId);
BookMapper.xml 추가
<select id="getLikeCount" parameterType="Integer" resultType="Integer">
select
count(*)
from
book_like_tb
where
book_id = #{bookId}
</select>
BookService 추가
BookController (getLikeCount 메서드 추가)
BookDetail.js 추가
const getLikeCount = useQuery(["getLikeCount"], async() => {
const option = {
headers:{
Authorization: localStorage.getItem("accessToken")
}
}
const response = axios.get(`http://localhost:8080/book/${bookId}/like`, option);
return response;
});
<p>분류: {getBook.data.data.categoryName} / 저자명: {getBook.data.data.authorName} / 출판사: {getBook.data.data.publisherName} / 추천: {getLikeCount.isLoading ? "조회중..." : getLikeCount.data.data} </p>
Book 추가
private int likeCount;
BookMapper.xml searchBooks에 join 추가
lc.like_count, join 추가
select
bt.book_id,
bt.book_name,
bt.author_id,
bt.publisher_id,
bt.category_id,
bt.cover_img_url,
lc.like_count,
at.author_id,
at.author_name,
pt.publisher_id,
pt.publisher_name,
ct.category_id,
ct.category_name
from
book_tb bt
left outer join author_tb at on (at.author_id = bt.author_id)
left outer join publisher_tb pt on (pt.publisher_id = bt.publisher_id)
left outer join category_tb ct on (ct.category_id = bt.category_id)
left outer join (select book_id, count(*) as like_count from book_like_tb group by book_id) lc on (lc.book_id = bt.book_id)
SearchBookRespDto 추가
private int likeCount;
BookCard.js (추천 추가)
<footer css={footer}>
<div css={like}><div css={likeIcon}><AiOutlineLike /></div>추천: {book.likeCount} </div>
<h2>저자명: {book.authorName}</h2>
<h2>출판사: {book.publisherName}</h2>
</footer>
BookMapper.xml (getLikeStatus 추가)
<select id="getLikeStatus" parameterType="hashMap" resultType="Integer">
select
count(*)
from
book_like_tb
where
book_id = #{bookId}
and user_id = #{userId}
</select>
BookRespository (getLikeStatus 추가)
public int getLikeStatus(Map<String, Object> map);
BookService (getLikeStatus 추가)
private final UserRepository userRepository;
public int getLikeStatus(int bookId) {
Map<String, Object> map = new HashMap<>();
map.put("bookId", bookId);
String email = SecurityContextHolder.getContext().getAuthentication().getName();
User userEntity = userRepository.findUserByEmail(email);
map.put("userId", userEntity.getUserId());
return bookRepository.getLikeStatus(map);
}