AWS Back Day 81. "Spring Boot를 활용한 검색 기능 및 상세 페이지 구현, 좋아요 기능과 쿼리 사용법"

이강용·2023년 4월 26일
0

Spring Boot

목록 보기
16/20

검색 기능 구현

  • front
    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>
    );
  • back

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;
    }
`;

클릭 시 상세 페이지 이동 구현

  • Front

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;

useQuery

  • React Query 라이브러리에서 제공하는 훅으로, 원격 데이터를 쉽게 가져오고 캐싱할 수 있는 기능을 제공함
특징설명
데이터 패칭원격 API로부터 데이터를 가져오는 과정을 캡슐화합니다.
캐싱가져온 데이터를 자동으로 캐싱하여 중복 요청을 방지하고 성능과 사용자 경험을 향상시킵니다.
자동 리패칭일정 시간 간격으로 데이터를 자동으로 리패칭하여 최신 상태로 유지할 수 있습니다.
상태 관리isLoading, isError, data, error 등의 상태를 쉽게 추적하여 UI를 쉽게 업데이트할 수 있습니다.
쿼리 취소요청이 더 이상 필요하지 않은 경우 자동으로 취소하여 불필요한 네트워크 트래픽을 방지합니다.
페이징 및 무한 스크롤useInfiniteQuery와 같은 추가 훅을 사용하여 페이징 또는 무한 스크롤 기능을 쉽게 구현할 수 있습니다.

Callback 함수 복습

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("다른 작업 수행");
  1. console.log("데이터 요청 시작"); 실행
  2. getUserData 함수호출, userId1과 콜백 함수를 인수로 전달
  3. getUserData 함수 내에서 setTimeout()이 호출되어, 1000ms(1초) 후에 콜백함수를 실행하도록 예약, 이 시간 동안, 코드는 다른 작업을 계속 수행
  4. console.log("다른 작업 수행")을 실행
  5. 1초가 지나면, setTimeout()에 전달된 화살표 함수가 실행, 이 함수는 데이터를 불러오는 중...을 출력하고, userData객체를 생성한 다음, callback(userData)를 호출하여 초기에 전달한 콜백 함수를 실행
  6. 콜백 함수가 실행되면, 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>
    );
};

서버에서 BookDetail 세부정보 받아오기

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만을 사용할 것을 장려

좋아요 기능 구현

  • MySQL

book_managemnet
book_like_tb 생성

  • Back

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 메서드 추가)

  • Front

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>
  • Back

Book 추가

private int likeCount;
  • Front
    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;
  • Front

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>
  • Back

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);
	}
profile
HW + SW = 1

0개의 댓글