AWS Back Day 83. "Spring Boot를 활용한 도서 관리 시스템 구축 및 사용자 권한 관리"

이강용·2023년 4월 28일
0

Spring Boot

목록 보기
18/20

관리자 계정 (Admin) 로그인

SideBar.js -> AuthRouteReactQuery.js (이동)

  • principal 안에 권한 정보가 들어 있기때문에, 이를 AuthRouteReactQuery.js 로 옮김

SideBar.js

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 요청
        });
        return response;
    });

AuthRouteReactQuery.js

const principal = useQuery(["principal"], async() => {
        const accessToken = localStorage.getItem("accessToken");
        const response = await axios.get("http://localhost:8080/auth/principal", {params: {accessToken}})
        return response;
    },{
       enabled: !!localStorage.getItem("accessToken")   // true 일때만 get 요청
    });
const Sidebar = () => {

    const [ isOpen, setIsOpen ] = useState(false);
    const queryClient = useQueryClient();
    

    const sidebarOpenClickHandle = () => {
        if(!isOpen){
            setIsOpen(true);
        }
    }

    const sidebarCloseClickHandle = () => {
        setIsOpen(false);
    }

    const logoutClickHandle = () => {
        if(window.confirm("로그아웃 하시겠습니까?")){
            localStorage.removeItem("accessToken");
        }
    }

    if(queryClient.getQueryState("principal").status === "loading"){
        return <div>로딩중...</div>
    }


    const principalData = queryClient.getQueryData("principal").data;

    return (
        <div css={ sidebar(isOpen) } onClick={sidebarOpenClickHandle}> 
            <header css={header}>
                <div css ={userIcon}>
                    {principalData.name.substr(0,1)} 
                </div>
                <div css={userInfo}>
                    <h1 css={userName}>{principalData.name}</h1>
                    <p css={userEmail}>{principalData.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>
    );
};

AuthRouteReactQuery (추가)

import axios from "axios";
import React, { useEffect, useState } from "react";
import { useQuery } from "react-query";
import { Navigate } from "react-router-dom";
import { refreshState } from "../../../../atoms/Auth/AuthAtoms";
import { useRecoilState } from "recoil";

const AuthRouteReactQuery = ({ path, element }) => {
    console.log("AuthRouteReactQuery 렌더링");

    const [refresh, setRefresh] = useRecoilState(refreshState);

    const { data, isLoading } = useQuery(
        ["authenticated"],
        async () => {
        const accessToken = localStorage.getItem("accessToken");
        const response = await axios.get("http://localhost:8080/auth/authenticated", {
            params: { accessToken },
        });
        return response;
        },
        {
        enabled: refresh,
        }
    );

    const principal = useQuery(
        ["principal"],
        async () => {
        const accessToken = localStorage.getItem("accessToken");
        const response = await axios.get("http://localhost:8080/auth/principal", {
            params: { accessToken },
        });
        return response;
        },
        {
        enabled: !!localStorage.getItem("accessToken"), // true 일때만 get 요청
        }
    );

    useEffect(() => {
        if (!refresh) {
        setRefresh(true);
        }
    }, [refresh]);

    // 경고 메시지 표시 상태 추가
    const [displayedAlert, setDisplayedAlert] = useState(false);

    if (isLoading) {
        return <div>로딩중...</div>;
    }

    if (!principal.isLoading) {
        const roles = principal.data.data.authorities.split(",");
        const hasAdminPath = path.startsWith("/admin");
        if (hasAdminPath && !roles.includes("ROLE_ADMIN") && !displayedAlert) {
        alert("접근 권한이 없습니다.");
        setDisplayedAlert(true); // 경고 메시지가 표시되었음을 표시
        
        }
    }

    if (!isLoading) {
        const permitAll = ["/login", "/register", "/password/forgot"];

        if (!data.data) {
        if (permitAll.includes(path)) {
            return element;
        }
        return <Navigate to="/login" />;
        }
        if (permitAll.includes(path)) {
        return <Navigate to="/" />;
        }
        return element;
    }
};

export default AuthRouteReactQuery;

App.js (수정)

function App() {

  return (
    <>
      <Global styles={Reset} />
      <Routes>
        <Route exact path="/login" element={<AuthRouteReactQuery path="/login" element={<Login />} />} />
        <Route path="/register" element={<AuthRouteReactQuery path="/register" element={<Register />} />} />
        <Route path="/" element={<AuthRouteReactQuery path="/" element={<Main />} />} />
        <Route path="/book/:bookId" element={<AuthRouteReactQuery path="/book" element={<BookDetail />} />} />
        <Route path="/admin/search" element={<AuthRouteReactQuery path="/admin/search" element={<Main />} />} />
        
      </Routes>
    </>
  );
}

라우터 연결

App.js

import { Global } from '@emotion/react';
import { Reset } from './styles/Global/reset';
import { Route, Routes } from 'react-router-dom';
import Login from './pages/Login/Login';
import Register from './pages/Register/Register';
import Main from './pages/Main/Main';
import AuthRouteReactQuery from './components/UI/Routes/AuthRoute/AuthRouteReactquery';
import BookDetail from './pages/BookDetail/BookDetail';
import BookRegister from './pages/Admin/BookRegister/BookRegister';

function App() {

  return (
    <>
      <Global styles={Reset} />
      <Routes>
        <Route exact path="/login" element={<AuthRouteReactQuery path="/login" element={<Login />} />} />
        <Route path="/register" element={<AuthRouteReactQuery path="/register" element={<Register />} />} />
        <Route path="/" element={<AuthRouteReactQuery path="/" element={<Main />} />} />
        <Route path="/book/:bookId" element={<AuthRouteReactQuery path="/book" element={<BookDetail />} />} />
        <Route path="/admin/book/register" element={<AuthRouteReactQuery path="/admin/book/register" element={<BookRegister />} />} />
      </Routes>
    </>
  );
}

export default App;

MySQL 권한 변경

http://localhost:3000/admin/book/register (확인)

도서 추가

도서 검색 시 도서 정보 불러오기

pages > Admin > BookRegister

BookRegister.js (디자인)

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import axios from 'axios';
import React, { useState } from 'react';
import { BiSearch } from 'react-icons/bi';
import { useMutation, useQuery } from 'react-query';



const tableContainer = css`
    height: 300PX;
    overflow: auto;
`;
const table = css`
    border: 1px solid #dbdbdb;

`;

const thAndTd = css`
    border: 1px solid #dbdbdb;
    padding: 5px 10px;
    text-align: center;
`;




const BookRegister = () => {

    //파라미터 정보
    const [ searchParams, setSearchParams ] = useState({page:1 , searchValue: ""});

    const getBooks = useQuery(["registerSearchBooks"],async () => {
        const option = {
            //get요청
            params : {
                ...searchParams
            },
            headers : {
                Authorization : localStorage.getItem("accessToken")
            }
        }
        return await axios.get("http://localhost:8080/books",option);
    });
    const registerBookList = useMutation(); 
    /**
     * 도서 검색:  도서명, 저자, 출판사 (사용자 편의성 )
     */

    return (
        <div>
            <div>
                <label >도서검색</label>
                <input type="text"  />
                <button><BiSearch /></button>
            </div>
            <div css={tableContainer}>
                <table css={table}>
                    <thead>
                        <td css={thAndTd}>선택</td>
                        <td css={thAndTd}>분류</td>
                        <td css={thAndTd}>도서명</td>
                        <td css={thAndTd}>저자명</td>
                        <td css={thAndTd}>출판사</td>
                    </thead>
                    <tbody>
                        {getBooks.isLoading ? "" : getBooks.data.data.bookList.map( book => (
                            <tr>
                                <td css={thAndTd}><input type='radio' name='selected' value ={book.bookId} /></td>
                                <td css={thAndTd}>{book.categoryName}</td>
                                <td css={thAndTd}>{book.bookName}</td>
                                <td css={thAndTd}>{book.authorName}</td>
                                <td css={thAndTd}>{book.publisherName}</td>
                            </tr>)
                        )}
                        
                    </tbody>
                </table>
            </div>
            <div>
                <button>&#60;</button>
                <button>1</button>
                <button>2</button>
                <button>3</button>
                <button>4</button>
                <button>5</button>
                <button>&#62;</button>
            </div>
            <div>
                <label >도서코드</label>
                <input type="text" readOnly/>
            </div>
            <div>
                <label >분류</label>
                <input type="text" readOnly/>
            </div>
            <div>
                <label>도서명</label>
                <input type="text" readOnly/>
            </div>
            <div>
                <label >저자</label>
                <input type="text" readOnly/>
            </div>
            <div>
                <label >출판사</label>
                <input type="text" readOnly/>
            </div>
            <div>
                <label >이미지경로</label>
                <input type="text" readOnly/>
            </div>
            <button>등록</button>
        </div>
        
    );
};

export default BookRegister;

선택 버튼 클릭 시 , 도서 정보 전달

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import axios from 'axios';
import React, { useState } from 'react';
import { BiSearch } from 'react-icons/bi';
import { useMutation, useQuery, useQueryClient } from 'react-query';



const tableContainer = css`
    height: 300PX;
    overflow: auto;
`;
const table = css`
    border: 1px solid #dbdbdb;
    font-size: 12PX;
`;

const thAndTd = css`
    border: 1px solid #dbdbdb;
    padding: 5px 10px;
    text-align: center;
`;




const BookRegister = () => {
    
   
    //파라미터 정보
    const [ searchParams, setSearchParams ] = useState({page:1 , searchValue: ""});
    const [ refresh, setRefresh ] = useState(true);
    const [ findBook, setFindBook ] = useState({
        bookId:"",
        bookName:" ",
        authorName:"",
        publisherName:"",
        categoryName:"",
        coverImgUrl:""
    });  

    const getBooks = useQuery(["registerSearchBooks"],async () => {
        const option = {
            //get요청
            params : {
                ...searchParams
            },
            headers : {
                Authorization : localStorage.getItem("accessToken")
            }
        }
        return await axios.get("http://localhost:8080/books",option);
    },{
        enabled: refresh,
        onSuccess : () => {
            setRefresh(false);
        }
    });
    const registerBookList = useMutation(); 
    /**
     * 도서 검색:  도서명, 저자, 출판사 (사용자 편의성 )
     */

    const searchInputHandle = (e) => {
        setSearchParams({ ...searchParams, searchValue: e.target.value });
    }

    const searchSubmitHandle = (e) => {

        if(e.type !== "click"){
            if(e.keyCode !== 13){
                return;
            }
        }
        setSearchParams({ ...searchParams, page: 1 });
        setRefresh(true);
    }

    const checkBookHandle = (e) => {

        if(!e.target.checked){
            return;
        }
        const book = getBooks.data.data.bookList.filter(book => book.bookId === parseInt(e.target.value))[0];
        setFindBook({ ...book });
    }


    const pagination = () => {
        if(getBooks.isLoading){
            return (<></>);
        }


        // page cal logic
        const nowPage = searchParams.page;
        const lastPage = getBooks.data.data.totalCount % 20 === 0
            ? getBooks.data.data.totalCount / 20 
            : Math.floor(getBooks.data.data.totalCount / 20) + 1; 
        const startIndex = nowPage % 5 === 0 ? nowPage - 4 : nowPage - (nowPage % 5) + 1;
        const endIndex = startIndex + 4 <= lastPage ? startIndex+4 : lastPage;
        
        const pageNumbers = [];

        for(let i= startIndex; i <= endIndex; i++){
            pageNumbers.push(i);
        }

        return (
             <> 
                <button disabled={nowPage === 1} onClick={() => {
                    setSearchParams({ ...searchParams, page: 1});
                    setRefresh(true);
                }}>&#60;&#60;</button>

                <button disabled={nowPage === 1} onClick={() => {
                    setSearchParams({ ...searchParams, page: nowPage - 1 });
                    setRefresh(true);
                }}>&#60;</button>

                {pageNumbers.map(page => (<button key={page} onClick={() => {
                    setSearchParams({ ...searchParams, page });
                    setRefresh(true);
                }} disabled={page === nowPage} >{page}</button>))}

                <button disabled={nowPage === lastPage} onClick={() => {
                    setSearchParams({ ...searchParams, page: nowPage + 1 });
                    setRefresh(true);
                }}>&#62;</button>

                <button disabled={nowPage === lastPage} onClick={() => {
                    setSearchParams({ ...searchParams, page: lastPage });
                    setRefresh(true);
                }}>&#62;&#62;</button>
             </>
        )
    }


    return (
        <div>
            <div>
                <label >도서검색</label>
                <input type="text" onChange={searchInputHandle} onKeyUp={searchSubmitHandle} />
                <button onClick={searchSubmitHandle}><BiSearch /></button>
            </div>
            <div css={tableContainer}>
                <table css={table}>
                    <thead>
                        <td css={thAndTd}>선택</td>
                        <td css={thAndTd}>분류</td>
                        <td css={thAndTd}>도서명</td>
                        <td css={thAndTd}>저자명</td>
                        <td css={thAndTd}>출판사</td>
                    </thead>
                    <tbody>
                        {getBooks.isLoading ? "" : getBooks.data.data.bookList.map( book => (
                            <tr key={book.bookId}>
                                <td css={thAndTd}><input type='radio' onChange={checkBookHandle} name='selected' value ={book.bookId} /></td>
                                <td css={thAndTd}>{book.categoryName}</td>
                                <td css={thAndTd}>{book.bookName}</td>
                                <td css={thAndTd}>{book.authorName}</td>
                                <td css={thAndTd}>{book.publisherName}</td>
                            </tr>)
                        )}
                        
                    </tbody>
                </table>
            </div>
            <div>
                {pagination()}
            </div>
            <div>
                <label >도서코드</label>
                <input type="text" value={findBook.bookId} readOnly/>
            </div>
            <div>
                <label >분류</label>
                <input type="text" value={findBook.categoryName} readOnly/>
            </div>
            <div>
                <label>도서명</label>
                <input type="text" value={findBook.bookName} readOnly/>
            </div>
            <div>
                <label >저자</label>
                <input type="text" value={findBook.authorName} readOnly/>
            </div>
            <div>
                <label >출판사</label>
                <input type="text" value={findBook.publisherName} readOnly/>
            </div>
            <div>
                <label >이미지경로</label>
                <input type="text" value={findBook.coverImgUrl} readOnly/>
            </div>
            <button>등록</button>
        </div>
        
    );
};

export default BookRegister;

번호를 눌러 페이징 처리

BookRegister.js

    const pagination = () => {
        if(getBooks.isLoading){
            return (<></>);
        }


        // page cal logic
        const nowPage = searchParams.page;
        const lastPage = getBooks.data.data.totalCount % 20 === 0
            ? getBooks.data.data.totalCount / 20 
            : Math.floor(getBooks.data.data.totalCount / 20) + 1; 
        const startIndex = nowPage % 5 === 0 ? nowPage - 4 : nowPage - (nowPage % 5) + 1;
        const endIndex = startIndex + 4 <= lastPage ? startIndex+4 : lastPage;
        
        const pageNumbers = [];

        for(let i= startIndex; i <= endIndex; i++){
            pageNumbers.push(i);
        }

        return (
             <> 
                <button disabled={nowPage === 1} onClick={() => {
                    setSearchParams({ ...searchParams, page: 1});
                    setRefresh(true);
                }}>&#60;&#60;</button>

                <button disabled={nowPage === 1} onClick={() => {
                    setSearchParams({ ...searchParams, page: nowPage - 1 });
                    setRefresh(true);
                }}>&#60;</button>

                {pageNumbers.map(page => (<button key={page} onClick={() => {
                    setSearchParams({ ...searchParams, page });
                    setRefresh(true);
                }} disabled={page === nowPage} >{page}</button>))}

                <button disabled={nowPage === lastPage} onClick={() => {
                    setSearchParams({ ...searchParams, page: nowPage + 1 });
                    setRefresh(true);
                }}>&#62;</button>

                <button disabled={nowPage === lastPage} onClick={() => {
                    setSearchParams({ ...searchParams, page: lastPage });
                    setRefresh(true);
                }}>&#62;&#62;</button>
             </>
        )
    }

도서 정보 등록하기

  • Server

BookController ( bookListRegister 추가)

@PostMapping("/admin/book/list")
	public ResponseEntity<?> bookListRegister(@RequestBody Map<String, Integer> requestMap)  {
		return ResponseEntity.ok().body(bookService.registeBookList(requestMap.get("bookId")));	
}

BookRepository (registeBookList 메서드 추가)

public int registeBookList (int bookId);

BookService ( registeBookList 메서드 추가)

public int registeBookList(int bookId) {
		return bookRepository.registeBookList(bookId);
}

BookMapper.xml (registeBookList insert문 추가)

<insert id="registeBookList" parameterType="Integer">
		insert into book_list_tb
		values (0, #{bookId}, now())
</insert>
  • Front

BookRegister.js (등록 버튼 이벤트 추가)

const registerBookList = useMutation(async (bookId) => {
        /**
         * 도서 검색:  도서명, 저자, 출판사 (사용자 편의성 )
         */
        const option = {
            headers: {
                "Content-Type" : "application/json",
                Authorization: localStorage.getItem("accessToken")
            }

        }
        return await axios.post("http://localhost:8080/admin/book/list",JSON.stringify({bookId}),option);

    }); 

<button onClick={() => {registerBookList.mutate(findBook.bookId)}} >등록</button>

권한이 있는 사용자만 접근 허용

  • Back

SecurityConfig

http.authorizeRequests()
		 	 .antMatchers("/auth/**")
		 	 .permitAll()
		 	 .antMatchers("/admin/**")
		 	 .hasRole("ADMIN")
		 	 .anyRequest()
		 	 .authenticated()
		 	 .and()
		 	 .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
		 	 .exceptionHandling()
		 	 .authenticationEntryPoint(jwtAuthenticationEntryPoint);

.antMatchers("/admin/**").hasRole("ADMIN") : 해당하는 URL에 대해서는 "ADMIN" 권한을 가진 사용자만 접근할 수 있도록 설정하는 부분

사용자 권한에 따른 Sidebar 메뉴 표시

  • Front

SideBar.js (추가)

const principalData = queryClient.getQueryData("principal").data;
    const roles  = principalData.authorities.split(",");

    return (
        <div css={ sidebar(isOpen) } onClick={sidebarOpenClickHandle}> 
            <header css={header}>
                <div css ={userIcon}>
                    {principalData.name.substr(0,1)} 
                </div>
                <div css={userInfo}>
                    <h1 css={userName}>{principalData.name}</h1>
                    <p css={userEmail}>{principalData.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>
                {roles.includes("ROLE_ADMIN") ? (<ListButton title="RegisterBookList"><BiListUl /></ListButton>) : ""}
            </main>
            <footer css ={footer}>
                <ListButton title="Logout" onClick={logoutClickHandle}><BiLogOut /></ListButton>
            </footer>
        </div>
    );

profile
HW + SW = 1

0개의 댓글