사용자 | |
---|---|
도서 조회 | 검색, 스크롤 페이징 |
도서 반납 | |
도서 좋아요 |
관리자 | |
---|---|
도서 등록 | |
도서 조회 | 검색, 번호 페이징 |
도서 수정 | |
도서 삭제 |
components
Sidebar.js
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React, { useState } from 'react';
import { GrFormClose } from 'react-icons/gr';
import ListButton from './ListButton/ListButton';
import { BiHome, BiListUl, BiLogOut, BiLike } from 'react-icons/bi';
import { useQuery } from 'react-query';
import axios from 'axios';
const sidebar = (isOpen) => css`
position: absolute;
display: flex;
left: ${isOpen ? "20px" : "-240px"};
flex-direction: column;
border: 1px solid #dbdbdb;
border-radius: 10px;
width: 250px;
box-shadow: -1px 0px 5px #dbdbdb;
transition: left 1s ease;
${isOpen? "" : `
cursor: pointer;
`}
${isOpen ? "" :
`&:hover {
left:-230px;
}`
}
`;
const header = css`
display: flex;
align-items: center;
margin-bottom: 15px;
padding: 10px;
`;
const userIcon = css`
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
border-radius: 8px;
width: 45px;
height: 45px;
background-color: #713fff;
color: white;
font-size: 30px;
font-weight: 600;
`;
const userInfo = css`
display: flex;
flex-direction: column;
justify-content: center;
`;
const userName = css`
font-size: 18px;
font-weight: 600;
padding: 5px;
padding-top: 0;
`;
const userEmail = css`
font-size: 12px;
`;
const closeButton = css`
position: absolute;
top:10px;
right:10px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid #dbdbdb;
padding-left: 0.3px;
width: 18px;
height: 18px;
border-radius: 50%;
font-size: 12px;
cursor: pointer;
&:active {
background-color: #fafafa;
}
`;
const main = css`
padding: 10px;
border-bottom: 1px solid #dbdbdb;
`;
const footer = css`
padding: 10px;
`;
const Sidebar = () => {
const [ isOpen, setIsOpen ] = useState(false);
const sidebarOpenClickHandle = () => {
if(!isOpen){
setIsOpen(true);
}
}
const sidebarCloseClickHandle = () => {
setIsOpen(false);
}
return (
<div css={ sidebar(isOpen) } onClick={sidebarOpenClickHandle}>
<header css={header}>
<div css ={userIcon}>
b
</div>
<div css={userInfo}>
이강용
bbb@gmail.com
</div>
<div css={closeButton} onClick={sidebarCloseClickHandle}><GrFormClose /></div>
</header>
<main css={main}>
<ListButton title="Dashboard"><BiHome /></ListButton>
<ListButton title="Likes"><BiLike /></ListButton>
<ListButton title="Rental"><BiListUl /></ListButton>
</main>
<footer css ={footer}>
<ListButton title="Logout"><BiLogOut /></ListButton>
</footer>
</div>
);
};
export default Sidebar;
Main
Main.js
import React from 'react';
import Sidebar from '../../components/Sidebar/Sidebar';
const Main = () => {
return (
<div>
<Sidebar></Sidebar>
</div>
);
};
export default Main;
components > Sidebar > ListButton
ListButton.js
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React from 'react';
const list = css`
display: flex;
align-items: center;
border-radius: 7px;
width: 100%;
padding: 5px;
cursor: pointer;
&:hover {
background-color: #fafafa;
}
`;
const listIcon = css`
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
`;
const listTitle = css`
display: flex;
align-items: center;
font-weight: 600;
`;
const ListButton = ({children, title}) => {
return (
<div css = {list}>
<div css={listIcon}>{children}</div>
<div css={listTitle}>{title}</div>
</div>
);
};
export default ListButton;
AuthenticationController (추가)
@GetMapping("/principal")
public ResponseEntity<?> principal(String accessToken) {
return ResponseEntity.ok().body(authenticationService.getPrincipal(accessToken));
}
AuthenticationService(추가)
public PrincipalRespDto getPrincipal(String accessToken) {
Claims claims = jwtTokenProvider.getClaims(jwtTokenProvider.getToken(accessToken));
User userEntity = userRepository.findUserByEmail(claims.getSubject()); //email
return PrincipalRespDto.builder()
.userId(userEntity.getUserId())
.email(userEntity.getEmail())
.name(userEntity.getName())
.authorities((String)claims.get("auth"))
.build();
}
dto > auth
PrincipalRespDto (생성)
package com.toyproject.bookmanagement.dto.auth;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
public class PrincipalRespDto {
private int userId;
private String email;
private String name;
private String authorities;
}
Sidebar (추가)
const Sidebar = () => {
const [ isOpen, setIsOpen ] = useState(false);
const { data, isLoading } = useQuery(["principal"], async() => {
const accessToken = localStorage.getItem("accessToken");
const response = await axios.get("http://localhost:8080/auth/principal",
{params: {accessToken}},
{
enabled: accessToken // true 일때만 get 요청
});
console.log(response);
return response;
});
const sidebarOpenClickHandle = () => {
if(!isOpen){
setIsOpen(true);
}
}
const sidebarCloseClickHandle = () => {
setIsOpen(false);
}
const logoutClickHandle = () => {
if(window.confirm("로그아웃 하시겠습니까?")){
localStorage.removeItem("accessToken");
}
}
if(isLoading) {
return <>로딩중...</>;
}
if(!isLoading)
return (
<div css={ sidebar(isOpen) } onClick={sidebarOpenClickHandle}>
<header css={header}>
<div css ={userIcon}>
{data.data.name.substr(0,1)}
</div>
<div css={userInfo}>
<h1 css={userName}>{data.data.name}</h1>
<p css={userEmail}>{data.data.email}</p>
</div>
<div css={closeButton} onClick={sidebarCloseClickHandle}><GrFormClose /></div>
</header>
<main css={main}>
<ListButton title="Dashboard"><BiHome /></ListButton>
<ListButton title="Likes"><BiLike /></ListButton>
<ListButton title="Rental"><BiListUl /></ListButton>
</main>
<footer css ={footer}>
<ListButton title="Logout" onClick={logoutClickHandle}><BiLogOut /></ListButton>
</footer>
</div>
);
};
sidebar.js (추가)
const logoutClickHandle = () => {
if(window.confirm("로그아웃 하시겠습니까?")){
localStorage.removeItem("accessToken");
}
}
<footer css ={footer}>
<ListButton title="Logout" onClick={logoutClickHandle}><BiLogOut /></ListButton>
</footer>
ListButton.js (추가)
const ListButton = ({children, title, onClick}) => {
return (
<div css = {list} onClick={onClick}>
<div css={listIcon}>{children}</div>
<div css={listTitle}>{title}</div>
</div>
);
};
로그아웃 버튼 클릭 시
UI > BookCard
BookCard.js
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React from 'react';
import { AiOutlineLike } from 'react-icons/ai';
const cardContainer = css`
display: flex;
flex-direction: column;
align-items: center;
margin: 20px;
border: 1px solid #dbdbdb;
border-radius: 7px;
box-shadow: 0px 0px 5px #dbdbdb;
width: 300px;
cursor: pointer;
&:hover {
box-shadow: 0px 0px 10px #dbdbdb;
}
&:active {
background-color: #fafafa;
}
`;
const header = css`
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
`;
const titleText = css`
font-weight: 600;
`;
const main = css`
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
`;
const imgBox = css`
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 10px;
border-radius:7px;
box-shadow: 0px 5px 5px #dbdbdb;
padding: 3px;
height: 200px;
background-color: #fafafa;
overflow: hidden;
`;
const img = css`
height: 100%;
`;
const rentalButton = css`
cursor: pointer;
&:hover {
background-color: #fafafa;
}
&:active {
background-color: #eee;
}
`;
const footer = css`
display: flex;
flex-direction: column;
align-items: center;
font-weight: 600;
font-size: 14px;
padding: 20px;
`;
const like = css`
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 10px;
border: 1px solid #dbdbdb;
border-radius: 7px;
padding: 10px;
height: 30px;
background-color: white;
font-weight: 600;
box-shadow: 0px 5px 5px #dbdbdb;
`;
const likeIcon = css`
padding-right: 5px;
`;
const BookCard = () => {
return (
<div css={cardContainer}>
<header css={header}>
<h1 css={titleText}> 내 통장 사용 설명서 (통장 7개로 시작하는 세상에서 제일 쉬운 재테크)</h1>
</header>
<main css={main}>
<div css ={imgBox}>
<img css={img} src="https://epbook.eplib.or.kr/resources/images/opms/9788901101101.jpg" alt="내 통장 사용 설명서 (통장 7개로 시작하는 세상에서 제일 쉬운 재테크)" />
</div>
</main>
<footer css={footer}>
<div css={like}><div css={likeIcon}><AiOutlineLike /></div>추천: 10 </div>
<h2>저자명: 이천</h2>
<h2>출판사: 웅진윙스</h2>
</footer>
</div>
);
};
export default BookCard;
pages > Main
Main.js
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React, { useEffect } from 'react';
import Sidebar from '../../components/Sidebar/Sidebar';
import BookCard from '../../components/UI/BookCard/BookCard';
const mainContainer = css`
padding: 10px;
`;
const header = css`
display: flex;
justify-content: space-between;
height: 100px;
`;
const main = css`
display: flex;
flex-wrap: wrap;
height: 750px;
overflow-y: auto;
`;
const Main = () => {
useEffect(() => {
},[]);
return (
<div css ={mainContainer}>
<Sidebar></Sidebar>
<header css={header}>
<div>도서검색</div>
<div>
<input type="search" />
</div>
</header>
<main css ={main}>
<BookCard></BookCard>
<BookCard></BookCard>
<BookCard></BookCard>
<BookCard></BookCard>
<BookCard></BookCard>
<BookCard></BookCard>
</main>
</div>
);
};
export default Main;
total
개수 필요controller
BookController 생성
package com.toyproject.bookmanagement.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.toyproject.bookmanagement.dto.book.SearchBookReqDto;
import com.toyproject.bookmanagement.service.BookService;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class BookController {
private final BookService bookService;
@GetMapping("/books")
public ResponseEntity<?> searchBooks(SearchBookReqDto searchBookReqDto){
return ResponseEntity.ok().body(bookService.searchBooks(searchBookReqDto));
}
}
dto > book(패키지)
SearchBookReqDto
package com.toyproject.bookmanagement.dto.book;
import lombok.Data;
@Data
public class SearchBookReqDto {
private int page;
private String searchValue;
private String categoryId;
}
mappers
BookMapper.xml (생성)
<?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)
order by
bt.book_id
limit #{index}, 20;
</select>
</mapper>
repository
BookRepository (생성)
MySQL
SELECT
*
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)
order by
bt.book_id
limit 0, 20;
1 = 0 (0~20)
2 = 20 (21~40)
3 = 40 (41~60)
4 = 60 (61~80)
5 = 80 (81~100)
(page - 1) x 20
entity
Book (생성)
package com.toyproject.bookmanagement.entity;
import com.toyproject.bookmanagement.dto.book.SearchBookRespDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Book {
private int bookId;
private String bookName;
private int authorId;
private int publisherId;
private int categoryId;
private String coverImgUrl;
private Author author;
private Publisher publisher;
private Category category;
public SearchBookRespDto toDto() {
return SearchBookRespDto.builder()
.bookId(bookId)
.bookName(bookName)
.authorId(authorId)
.authorName(author.getAuthorName())
.publisherId(publisherId)
.publisherName(publisher.getPublisherName())
.categoryId(categoryId)
.categoryName(category.getCategoryName())
.coverImgUrl(coverImgUrl)
.build();
}
}
Author (생성)
package com.toyproject.bookmanagement.entity;
public class Author {
private int authorId;
private String authorName;
}
Publisher (생성)
package com.toyproject.bookmanagement.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Author {
private int authorId;
private String authorName;
}
Category (생성)
package com.toyproject.bookmanagement.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Category {
private int categoryId;
private String categoryName;
}
service
BookService (생성)
package com.toyproject.bookmanagement.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.toyproject.bookmanagement.dto.book.SearchBookReqDto;
import com.toyproject.bookmanagement.dto.book.SearchBookRespDto;
import com.toyproject.bookmanagement.repository.BookRepository;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class BookService {
private final BookRepository bookRepository;
public List<SearchBookRespDto> 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);
bookRepository.searchBooks(map).forEach(book -> {
list.add(book.toDto());
});
return list;
}
}
dto > book
SearchBookRespDto(생성)
package com.toyproject.bookmanagement.dto.book;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
public class SearchBookRespDto {
private int bookId;
private String bookName;
private int authorId;
private String authorName;
private int publisherId;
private String publisherName;
private int categoryId;
private String categoryName;
private String coverImgUrl;
}