AWS Back Day 79. "Spring Boot를 활용한 도서관리 시스템 : 사이드바, 로그아웃 구현 및 서버 도서 목록 페이징 처리 완성"

이강용·2023년 4월 24일
0

Spring Boot

목록 보기
14/20

도서관리 시스템 기능정의

사용자
도서 조회검색, 스크롤 페이징
도서 반납
도서 좋아요
관리자
도서 등록
도서 조회검색, 번호 페이징
도서 수정
도서 삭제

메인 page

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;

Server (backend)

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

Front

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

로그아웃 버튼 클릭 시

main 구현

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;

Server에서 도서정보 받아오기

페이징 처리

  • 책을 몇 권씩 들고올 것인가?
    • 책의 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;
}
profile
HW + SW = 1

0개의 댓글