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><</button>
<button>1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<button>5</button>
<button>></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);
}}><<</button>
<button disabled={nowPage === 1} onClick={() => {
setSearchParams({ ...searchParams, page: nowPage - 1 });
setRefresh(true);
}}><</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);
}}>></button>
<button disabled={nowPage === lastPage} onClick={() => {
setSearchParams({ ...searchParams, page: lastPage });
setRefresh(true);
}}>>></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);
}}><<</button>
<button disabled={nowPage === 1} onClick={() => {
setSearchParams({ ...searchParams, page: nowPage - 1 });
setRefresh(true);
}}><</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);
}}>></button>
<button disabled={nowPage === lastPage} onClick={() => {
setSearchParams({ ...searchParams, page: lastPage });
setRefresh(true);
}}>>></button>
</>
)
}
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>
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>
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.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>
);