.gitignore
package-lock.json
package.json
public
README.md
src
tailwind.config.js
===========================================================
\src\api\cartApi.js
===========================================================
import jwtAxios from "../util/jwtUtil"
import { API_SERVER_HOST } from "./todoApi"
const host = `${API_SERVER_HOST}/api/cart`
export const getCartItems = async ( ) => {
const res = await jwtAxios.get(`${host}/items`)
return res.data
}
export const postChangeCart = async (cartItem) => {
const res = await jwtAxios.post(`${host}/change`, cartItem)
return res.data
}
===========================================================
\src\api\kakaoApi.js
===========================================================
import axios from "axios";
import { API_SERVER_HOST } from "./todoApi";
const rest_api_key =`a09fff59f94e1899d5fb97fba7c87d00` //REST키값
const redirect_uri =`http://localhost:3000/member/kakao`
const auth_code_path = `https://kauth.kakao.com/oauth/authorize`
const access_token_url =`https://kauth.kakao.com/oauth/token` //추가
export const getKakaoLoginLink = () => {
const kakaoURL = `${auth_code_path}?client_id=${rest_api_key}&redirect_uri=${redirect_uri}&response_type=code`;
return kakaoURL
}
export const getAccessToken = async (authCode) => {
const header = {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
}
}
const params = {
grant_type: "authorization_code",
client_id: rest_api_key,
redirect_uri: redirect_uri,
code:authCode
}
const res = await axios.post(access_token_url, params , header)
const accessToken = res.data.access_token
return accessToken
}
export const getMemberWithAccessToken = async(accessToken) => {
const res = await axios.get(`${API_SERVER_HOST}/api/member/kakao?accessToken=${accessToken}`)
return res.data
}
===========================================================
\src\api\memberApi.js
===========================================================
import axios from "axios"
import { API_SERVER_HOST } from "./todoApi"
import jwtAxios from "../util/jwtUtil"
const host = `${API_SERVER_HOST}/api/member`
export const loginPost = async (loginParam) => {
const header = {headers: {"Content-Type": "x-www-form-urlencoded"}}
const form = new FormData()
form.append('username', loginParam.email)
form.append('password', loginParam.pw)
const res = await axios.post(`${host}/login`, form, header)
return res.data
}
export const modifyMember = async (member) => {
const res = await jwtAxios.put(`${host}/modify`, member)
return res.data
}
===========================================================
\src\api\productsApi.js
===========================================================
import axios from "axios"
import { API_SERVER_HOST } from "./todoApi"
import jwtAxios from "../util/jwtUtil"
const host = `${API_SERVER_HOST}/api/products`
export const postAdd = async (product) => {
const header = {headers: {"Content-Type": "multipart/form-data"}}
// 경로 뒤 '/' 주의
const res = await jwtAxios.post(`${host}/`, product, header)
return res.data
}
export const getList = async ( pageParam ) => {
const {page,size} = pageParam
const res = await jwtAxios.get(`${host}/list`, {params: {page:page,size:size }})
return res.data
}
export const getOne = async (tno) => {
const res = await jwtAxios.get(`${host}/${tno}` )
return res.data
}
export const putOne = async (pno, product) => {
const header = {headers: {"Content-Type": "multipart/form-data"}}
const res = await jwtAxios.put(`${host}/${pno}`, product, header)
return res.data
}
export const deleteOne = async (pno) => {
const res = await jwtAxios.delete(`${host}/${pno}`)
return res.data
}
===========================================================
\src\api\todoApi.js
===========================================================
import axios from "axios"
import jwtAxios from "../util/jwtUtil"
export const API_SERVER_HOST = 'http://localhost:8080'
const prefix = `${API_SERVER_HOST}/api/todo`
export const getOne = async (tno) => {
const res = await jwtAxios.get(`${prefix}/${tno}` )
return res.data
}
export const getList = async ( pageParam ) => {
const {page,size} = pageParam
const res = await jwtAxios.get(`${prefix}/list`, {params: {page:page,size:size }})
return res.data
}
export const postAdd = async (todoObj) => {
const res = await jwtAxios.post(`${prefix}/` , todoObj)
return res.data
}
export const deleteOne = async (tno) => {
const res = await jwtAxios.delete(`${prefix}/${tno}` )
return res.data
}
export const putOne = async (todo) => {
const res = await jwtAxios.put(`${prefix}/${todo.tno}`, todo)
return res.data
}
===========================================================
\src\App.css
===========================================================
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
===========================================================
\src\App.js
===========================================================
import {RouterProvider} from "react-router-dom";
import root from "./router/root";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient()
function App() {
return (
);
}
export default App;
===========================================================
\src\App.test.js
===========================================================
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render();
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
===========================================================
\src\atoms\cartState.js
===========================================================
import { atom, selector } from "recoil";
export const cartState = atom({
key:'cartState',
default:[]
})
export const cartTotalState = selector( {
key: "cartTotalState",
get: ( {get} ) => {
const arr = get(cartState)
const initialValue = 0
const total = arr.reduce((total , current) => total + current.price * current.qty , initialValue)
return total
}
})
===========================================================
\src\atoms\signinState.js
===========================================================
import { atom } from "recoil";
import { getCookie } from "../util/cookieUtil";
const initState = {
email:'',
nickname:'',
social: false,
accessToken:'',
refreshToken:''
}
const loadMemberCookie = () => { //쿠키에서 체크
const memberInfo = getCookie("member")
//닉네임 처리
if(memberInfo && memberInfo.nickname) {
memberInfo.nickname = decodeURIComponent(memberInfo.nickname)
}
return memberInfo
}
const signinState = atom({
key:'signinState',
default: loadMemberCookie() || initState
})
export default signinState
===========================================================
\src\components\cart\CartItemComponent.js
===========================================================
import { API_SERVER_HOST } from "../../api/todoApi";
const host = API_SERVER_HOST
const CartItemComponent = ({cino, pname, price, pno, qty, imageFile, changeCart, email}) => {
const handleClickQty = (amount) => {
changeCart({ email, cino, pno, qty: qty + amount})
}
return (
Cart Item No: {cino}
Pno: {pno}
Name: {pname}
Price: {price}
Qty: {qty}
{qty * price} 원
);
}
export default CartItemComponent;
===========================================================
\src\components\common\FetchingModal.js
===========================================================
const FetchingModal = ( ) => {
return (
);
}
export default FetchingModal;
===========================================================
\src\components\common\PageComponent.js
===========================================================
const PageComponent = ({serverData, movePage}) => {
return (
{serverData.prev ?
movePage({page:serverData.prevPage} )}>
Prev
: <>>}
{serverData.pageNumList.map(pageNum =>
movePage( {page:pageNum})}>
{pageNum}
)}
{serverData.next ?
movePage( {page:serverData.nextPage})}>
Next
: <>>}
);
}
export default PageComponent;
===========================================================
\src\components\common\ResultModal.js
===========================================================
const ResultModal = ( {title,content, callbackFn} ) => {
return (
{
if(callbackFn) {
callbackFn()
}
}}>
{title}
{content}
);
}
export default ResultModal;
===========================================================
\src\components\member\KakaoLoginComponent.js
===========================================================
import { Link } from "react-router-dom";
import { getKakaoLoginLink } from "../../api/kakaoApi";
const KakaoLoginComponent = () => {
const link = getKakaoLoginLink()
return (
)
}
export default KakaoLoginComponent;
===========================================================
\src\components\member\LoginComponent.js
===========================================================
import { useState } from "react"
import useCustomLogin from "../../hooks/useCustomLogin"
import KakaoLoginComponent from "./KakaoLoginComponent"
const initState = {
email:'',
pw:''
}
const LoginComponent = () => {
const [loginParam, setLoginParam] = useState({...initState})
const {doLogin, moveToPath} = useCustomLogin()
const handleChange = (e) => {
loginParam[e.target.name] = e.target.value
setLoginParam({...loginParam})
}
const handleClickLogin = (e) => {
doLogin(loginParam) // loginSlice의 비동기 호출
.then(data => {
console.log(data)
if(data.error) {
alert("이메일과 패스워드를 다시 확인하세요")
}else {
alert("로그인 성공")
moveToPath('/')
}
})
}
return (
);
}
export default LoginComponent;
===========================================================
\src\components\member\LogoutComponent.js
===========================================================
import useCustomLogin from "../../hooks/useCustomLogin";
const LogoutComponent = () => {
const {doLogout, moveToPath} = useCustomLogin()
const handleClickLogout = () => {
doLogout()
alert("로그아웃되었습니다.")
moveToPath("/")
}
return (
);
}
export default LogoutComponent;
===========================================================
\src\components\member\ModifyComponent.js
===========================================================
import { useEffect } from "react";
import { useState } from "react";
import { useSelector } from "react-redux";
import { modifyMember } from "../../api/memberApi";
import useCustomLogin from "../../hooks/useCustomLogin";
import ResultModal from "../common/ResultModal";
const initState = {
email: '',
pw:'',
nickname:''
}
const ModifyComponent = () => {
const [member, setMember] = useState(initState)
const loginInfo = useSelector(state => state.loginSlice)
const {moveToLogin} = useCustomLogin()
const [result, setResult] = useState()
useEffect(() => {
setMember({...loginInfo, pw:'ABCD'})
},[loginInfo])
const handleChange = (e) => {
member[e.target.name] = e.target.value
setMember({...member})
}
const handleClickModify = () => {
modifyMember(member).then(result => {
setResult('Moodified')
})
}
const colseModal = () => {
setResult(null)
moveToLogin()
}
return (
);
}
export default ModifyComponent;
===========================================================
\src\components\menus\BasicMenu.js
===========================================================
import { Link } from "react-router-dom";
import useCustomLogin from "../../hooks/useCustomLogin";
const BasicMenu = () => {
const {loginState} = useCustomLogin()
return (
);
}
export default BasicMenu;
===========================================================
\src\components\menus\CartComponent.js
===========================================================
import useCustomLogin from "../../hooks/useCustomLogin";
import useCustomCart from "../../hooks/useCustomCart";
import CartItemComponent from "../cart/CartItemComponent";
import { useRecoilValue } from "recoil";
import { cartTotalState } from "../../atoms/cartState";
const CartComponent = () => {
const {isLogin, loginState} = useCustomLogin()
const { cartItems, changeCart } = useCustomCart()
const totalValue = useRecoilValue(cartTotalState)
return (
{isLogin ?
{loginState.nickname}'s Cart
{cartItems.length}
{cartItems.map( item =>
)}
TOTAL: {totalValue}
:
}
);
}
export default CartComponent;
===========================================================
\src\components\products\AddComponent.js
===========================================================
import { useRef, useState } from "react";
import { postAdd } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import ResultModal from "../common/ResultModal";
import useCustomMove from "../../hooks/useCustomMove";
import { useMutation, useQueryClient } from "@tanstack/react-query";
const initState = {
pname: '',
pdesc: '',
price: 0,
files: []
}
const AddComponent = () => {
//기본적으로 필요
const [product,setProduct] = useState({...initState})
const uploadRef = useRef()
const {moveToList} = useCustomMove()
//입력값 처리
const handleChangeProduct = (e) => {
product[e.target.name] = e.target.value
setProduct({...product})
}
const addMutation = useMutation( (product) => postAdd(product)) //리액트 쿼리
const handleClickAdd = (e) => {
const files = uploadRef.current.files
const formData = new FormData()
for (let i = 0; i < files.length; i++) {
formData.append("files", files[i]);
}
//other data
formData.append("pname", product.pname)
formData.append("pdesc", product.pdesc)
formData.append("price", product.price)
addMutation.mutate( formData ) //기존 코드에서 변경
}
const queryClient = useQueryClient()
const closeModal = () => {
queryClient.invalidateQueries("products/list")
moveToList({page:1})
}
return (
{addMutation.isLoading ?
: <>>}
{addMutation.isSuccess ?
:
<>>
}
);
}
export default AddComponent;
===========================================================
\src\components\products\ListComponent.js
===========================================================
import { getList } from "../../api/productsApi";
import useCustomMove from "../../hooks/useCustomMove";
import FetchingModal from "../common/FetchingModal";
import { API_SERVER_HOST } from "../../api/todoApi";
import PageComponent from "../common/PageComponent";
import useCustomLogin from "../../hooks/useCustomLogin";
import { useQuery } from "@tanstack/react-query";
const initState = {
dtoList:[],
pageNumList:[],
pageRequestDTO: null,
prev: false,
next: false,
totoalCount: 0,
prevPage: 0,
nextPage: 0,
totalPage: 0,
current: 0
}
const host = API_SERVER_HOST
const ListComponent = () => {
const {moveToLoginReturn} = useCustomLogin()
const {page, size, refresh, moveToList, moveToRead} = useCustomMove()
const {isFetching, data, error, isError} = useQuery(
['products/list' , {page,size, refresh}],
() => getList({page,size}),
{staleTime: 1000 * 5 }
)
const handleClickPage = (pageParam) => {
// if(pageParam.page === parseInt(page)){
// queryClient.invalidateQueries("products/list")
// }
moveToList(pageParam)
}
if(isError) {
console.log(error)
return moveToLoginReturn()
}
const serverData = data || initState
return (
{isFetching?
:<>>}
{serverData.dtoList.map(product =>
moveToRead(product.pno)}
>
{product.pno}
이름: {product.pname}
가격: {product.price}
)}
);
}
export default ListComponent;
===========================================================
\src\components\products\ModifyComponent.js
===========================================================
import { useEffect, useRef, useState } from "react";
import { deleteOne, getOne, putOne } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import { API_SERVER_HOST } from "../../api/todoApi";
import useCustomMove from "../../hooks/useCustomMove";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import ResultModal from "../common/ResultModal";
const initState = {
pno:0,
pname: '',
pdesc: '',
price: 0,
delFlag:false,
uploadFileNames:[]
}
const host = API_SERVER_HOST
const ModifyComponent = ({pno}) => {
const {moveToList, moveToRead} = useCustomMove()
const [product, setProduct] = useState(initState)
const uploadRef = useRef()
const query = useQuery(
['products', pno],
() => getOne(pno),
{
staleTime: Infinity
}
)
useEffect(() => {
if(query.isSuccess){
setProduct(query.data)
}
},[pno, query.data, query.isSuccess])
const handleChangeProduct = (e) => {
product[e.target.name] = e.target.value
setProduct({...product})
}
const deleteOldImages = (imageName) => {
const resultFileNames = product.uploadFileNames.filter( fileName => fileName !== imageName)
product.uploadFileNames = resultFileNames
setProduct({...product})
}
const delMutation = useMutation((pno) => deleteOne(pno))
const queryClient = useQueryClient()
const handleClickDelete = () => {
delMutation.mutate(pno)
}
const modMutation = useMutation((product) => putOne(pno, product))
const handleClickModify = () => {
const files = uploadRef.current.files
const formData = new FormData()
for (let i = 0; i < files.length; i++) {
formData.append("files", files[i]);
}
//other data
formData.append("pname", product.pname)
formData.append("pdesc", product.pdesc)
formData.append("price", product.price)
formData.append("delFlag", product.delFlag)
for( let i = 0; i < product.uploadFileNames.length ; i++){
formData.append("uploadFileNames", product.uploadFileNames[i])
}
modMutation.mutate(formData)
}
const closeModal = () => {
if(delMutation.isSuccess) {
queryClient.invalidateQueries(['products', pno])
queryClient.invalidateQueries(['products/list'])
moveToList()
return
}
if(modMutation.isSuccess) {
queryClient.invalidateQueries(['products', pno])
queryClient.invalidateQueries(['products/list'])
moveToRead(pno)
}
}
return (
{query.isFetching || delMutation.isLoading || modMutation.isLoading ?
:
<>>
}
{
delMutation.isSuccess || modMutation.isSuccess ?
:
<>>
}
Images
{product.uploadFileNames.map( (imgFile, i) =>
)}
);
}
export default ModifyComponent;
===========================================================
\src\components\products\ReadComponent.js
===========================================================
import {getOne} from "../../api/productsApi"
import { API_SERVER_HOST } from "../../api/todoApi"
import useCustomCart from "../../hooks/useCustomCart"
import useCustomLogin from "../../hooks/useCustomLogin"
import useCustomMove from "../../hooks/useCustomMove"
import FetchingModal from "../common/FetchingModal"
import { useQuery } from "@tanstack/react-query"
const initState = {
pno:0,
pname: '',
pdesc: '',
price: 0,
uploadFileNames:[]
}
const host = API_SERVER_HOST
const ReadComponent = ({pno }) => {
const {moveToList, moveToModify} = useCustomMove()
const {loginState} = useCustomLogin()
const {cartItems, changeCart} = useCustomCart()
const {isFetching, data } = useQuery(
['products', pno],
( ) => getOne(pno),
{
staleTime: 1000 * 10 * 60,
retry: 1
}
)
const handleClickAddCart = () => {
let qty = 1
const addedItem = cartItems.filter(item => item.pno === parseInt(pno))[0]
if(addedItem) {
if(window.confirm("이미 추가된 상품입니다. 추가하시겠습니까? ") === false) {
return
}
qty = addedItem.qty + 1
}
changeCart({email:loginState.email, pno:pno, qty:qty})
}
const product = data || initState
return (
{isFetching?
:<>>}
{product.uploadFileNames.map( (imgFile, i) =>
)}
)
}
export default ReadComponent
===========================================================
\src\components\todo\AddComponent.js
===========================================================
import { useState } from "react";
import { postAdd } from "../../api/todoApi";
import ResultModal from "../common/ResultModal";
import useCustomMove from "../../hooks/useCustomMove";
const initState = {
title:'',
writer: '',
dueDate: ''
}
const AddComponent = () => {
const [todo, setTodo] = useState({...initState})
const [result, setResult] = useState(null) //결과 상태
const {moveToList} = useCustomMove() //useCustomMove 활용
const handleChangeTodo = (e) => {
todo[e.target.name] = e.target.value
setTodo({...todo})
}
const handleClickAdd = () => {
//console.log(todo)
postAdd(todo)
.then(result => {
console.log(result)
setResult(result.TNO) //결과 데이터 변경
setTodo({...initState})
}).catch(e => {
console.error(e)
})
}
const closeModal = () => {
setResult(null)
moveToList() //moveToList( )호출
}
return (
{/* 모달 처리 */}
{result ?
: <>>}
);
}
export default AddComponent;
===========================================================
\src\components\todo\ListComponent.js
===========================================================
import { useEffect, useState } from "react";
import { getList } from "../../api/todoApi";
import useCustomMove from "../../hooks/useCustomMove";
import PageComponent from "../common/PageComponent";
const initState = {
dtoList:[],
pageNumList:[],
pageRequestDTO: null,
prev: false,
next: false,
totoalCount: 0,
prevPage: 0,
nextPage: 0,
totalPage: 0,
current: 0
}
const ListComponent = () => {
const {page, size, refresh, moveToList, moveToRead} = useCustomMove()//refresh
//serverData는 나중에 사용
const [serverData, setServerData] = useState(initState)
useEffect(() => {
getList({page,size}).then(data => {
console.log(data)
setServerData(data)
})
}, [page,size, refresh])
return (
{serverData.dtoList.map(todo =>
moveToRead(todo.tno)} //이벤트 처리 추가
>
{todo.tno}
{todo.title}
{todo.dueDate}
)}
);
}
export default ListComponent;
===========================================================
\src\components\todo\ModifyComponent.js
===========================================================
import { useCallback, useEffect, useState } from "react";
import { deleteOne, getOne, putOne } from "../../api/todoApi";
import ResultModal from "../common/ResultModal";
import useCustomMove from "../../hooks/useCustomMove";
const initState = {
tno:0,
title:'',
writer: '',
dueDate: '',
complete: false
}
const ModifyComponent = ({tno, moveList, moveRead}) => {
const [todo, setTodo] = useState({...initState})
//모달 창을 위한 상태
const [result, setResult] = useState(null)
//이동을 위한 기능들
const {moveToList, moveToRead} = useCustomMove()
const handleClickModify = () => { //버튼 클릭시
//console.log(todo)
putOne(todo).then(data => {
console.log("modify result: " + data)
setResult('Modified')
})
}
const handleClickDelete = () => { //버튼 클릭시
deleteOne(tno).then( data => {
console.log("delete result: " + data)
setResult('Deleted')
})
}
//모달 창이 close될때
const closeModal = () => {
if(result ==='Deleted') {
moveToList()
}else {
moveToRead(tno)
}
}
useEffect(() => {
getOne(tno).then(data => setTodo(data))
},[tno])
const handleChangeTodo = (e) => {
todo[e.target.name] = e.target.value
setTodo({...todo})
}
const handleChangeTodoComplete = (e) => {
const value = e.target.value
todo.complete = (value === 'Y')
setTodo({...todo})
}
return (
{result ?
:<>>}
COMPLETE
);
}
export default ModifyComponent;
===========================================================
\src\components\todo\ReadComponent.js
===========================================================
import { useEffect, useState } from "react"
import {getOne} from "../../api/todoApi"
import useCustomMove from "../../hooks/useCustomMove"
const initState = {
tno:0,
title:'',
writer: '',
dueDate: null,
complete: false
}
const ReadComponent = ({tno}) => {
const [todo, setTodo] = useState(initState) //아직 todo는 사용하지 않음
const {moveToList, moveToModify} = useCustomMove()
useEffect(() => {
getOne(tno).then(data => {
console.log(data)
setTodo(data)
})
}, [tno])
return (
{makeDiv('Tno', todo.tno)}
{makeDiv('Writer', todo.writer)}
{makeDiv('Title', todo.title)}
{makeDiv('Due Date', todo.dueDate)}
{makeDiv('Complete', todo.complete ? 'Completed' : 'Not Yet')}
{/* buttons.........start */}
)
}
const makeDiv = (title,value) =>
export default ReadComponent
===========================================================
\src\hooks\useCustomCart.js
===========================================================
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { getCartItems, postChangeCart } from "../api/cartApi"
import { useRecoilState } from "recoil"
import { cartState } from "../atoms/cartState"
import { useEffect } from "react"
const useCustomCart = () => {
const [cartItems,setCartItems] = useRecoilState(cartState)
const queryClient = useQueryClient()
const changeMutation = useMutation((param) => postChangeCart(param), {onSuccess: (result) => {
setCartItems(result)
}})
const query = useQuery(["cart"], getCartItems, {staleTime: 1000 * 60 * 60}) // 1 hour
useEffect(() => {
if(query.isSuccess || changeMutation.isSuccess) {
queryClient.invalidateQueries("cart")
setCartItems(query.data)
}
},[query.isSuccess, query.data])
const changeCart = (param) => {
changeMutation.mutate(param)
}
return {cartItems, changeCart}
}
export default useCustomCart
===========================================================
\src\hooks\useCustomLogin.js
===========================================================
import { Navigate, createSearchParams, useNavigate } from "react-router-dom"
import { useRecoilState, useResetRecoilState } from "recoil"
import signinState from "../atoms/signinState"
import { loginPost } from "../api/memberApi"
import { removeCookie, setCookie } from "../util/cookieUtil"
import { cartState } from "../atoms/cartState"
const useCustomLogin = ( ) => {
const navigate = useNavigate()
const [loginState, setLoginState] = useRecoilState(signinState)
const resetState = useResetRecoilState(signinState)
const resetCartState = useResetRecoilState(cartState) //장바구니 비우기
const isLogin = loginState.email ? true :false //----------로그인 여부
const doLogin = async (loginParam) => { //----------로그인 함수
const result = await loginPost(loginParam)
console.log(result)
saveAsCookie(result)
return result
}
const saveAsCookie = (data) => {
setCookie("member",JSON.stringify(data), 1) //1일
setLoginState(data)
}
const doLogout = () => { //---------------로그아웃 함수
removeCookie('member')
resetState()
resetCartState()
}
const exceptionHandle = (ex) => {
console.log("Exception------------------------")
console.log(ex)
const errorMsg = ex.response.data.error
const errorStr = createSearchParams({error: errorMsg}).toString()
if(errorMsg === 'REQUIRE_LOGIN'){
alert("로그인 해야만 합니다.")
navigate({pathname:'/member/login' , search: errorStr})
return
}
if(ex.response.data.error === 'ERROR_ACCESSDENIED'){
alert("해당 메뉴를 사용할 수 있는 권한이 없습니다.")
navigate({pathname:'/member/login' , search: errorStr})
return
}
}
const moveToPath = (path) => { //----------------페이지 이동
navigate({pathname: path}, {replace:true})
}
const moveToLogin = () => { //----------------------로그인 페이지로 이동
navigate({pathname: '/member/login'}, {replace:true})
}
const moveToLoginReturn = () => { //----------------------로그인 페이지로 이동 컴포넌트
return
}
return {loginState, isLogin, doLogin, doLogout, moveToPath, moveToLogin, moveToLoginReturn, exceptionHandle, saveAsCookie}
}
export default useCustomLogin
===========================================================
\src\hooks\useCustomMove.js
===========================================================
import { useState } from "react"
import { createSearchParams, useNavigate, useSearchParams } from "react-router-dom"
const getNum = (param, defaultValue) => {
if(!param){
return defaultValue
}
return parseInt(param)
}
const useCustomMove = () => {
const navigate = useNavigate()
const [refresh, setRefresh] = useState(false)
const [queryParams] = useSearchParams()
const page = getNum(queryParams.get('page'), 1)
const size = getNum(queryParams.get('size'),10)
const queryDefault = createSearchParams({page, size}).toString() //새로 추가
const moveToList = (pageParam) => {
let queryStr = ""
if(pageParam){
const pageNum = getNum(pageParam.page, 1)
const sizeNum = getNum(pageParam.size, 10)
queryStr = createSearchParams({page:pageNum, size: sizeNum}).toString()
}else {
queryStr = queryDefault
}
navigate({
pathname: `../list`,
search:queryStr
})
setRefresh(!refresh) //추가
}
const moveToModify = (num) => {
console.log(queryDefault)
navigate({
pathname: `../modify/${num}`,
search: queryDefault //수정시에 기존의 쿼리 스트링 유지를 위해
})
}
const moveToRead =(num) => {
console.log(queryDefault)
navigate({
pathname: `../read/${num}`,
search: queryDefault
})
}
return {moveToList, moveToModify, moveToRead, page, size, refresh} //refresh 추가
}
export default useCustomMove
===========================================================
\src\index.css
===========================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
===========================================================
\src\index.js
===========================================================
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from './store'
import { RecoilRoot } from 'recoil';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
reportWebVitals();
===========================================================
\src\layouts\BasicLayout.js
===========================================================
import BasicMenu from "../components/menus/BasicMenu";
import CartComponent from "../components/menus/CartComponent";
const BasicLayout = ({children}) => {
return (
<>
{/* 기존 헤더 대신 BasicMenu*/ }
{/* 상단 여백 my-5 제거 */}
{/* 상단 여백 py-40 변경 flex 제거 */}
{children}
>
);
}
export default BasicLayout;
===========================================================
\src\logo.svg
===========================================================
===========================================================
\src\pages\AboutPage.js
===========================================================
import useCustomLogin from "../hooks/useCustomLogin";
import BasicLayout from "../layouts/BasicLayout";
const AboutPage = () => {
const {isLogin, moveToLoginReturn} = useCustomLogin()
if(!isLogin){
return moveToLoginReturn()
}
return (
About Page
);
}
export default AboutPage;
===========================================================
\src\pages\MainPage.js
===========================================================
import BasicLayout from "../layouts/BasicLayout";
const MainPage = () => {
return (
Main Page
);
}
export default MainPage;
===========================================================
\src\pages\member\KakaoRedirectPage.js
===========================================================
import { useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import { getAccessToken, getMemberWithAccessToken } from "../../api/kakaoApi";
import useCustomLogin from "../../hooks/useCustomLogin";
const KakaoRedirectPage = () => {
const [searchParams] = useSearchParams()
const {moveToPath, saveAsCookie} = useCustomLogin()
const authCode = searchParams.get("code")
useEffect(() => {
getAccessToken(authCode).then(accessToken => {
console.log(accessToken)
getMemberWithAccessToken(accessToken).then(memberInfo => {
console.log("-------------------")
console.log(memberInfo)
saveAsCookie(memberInfo)
//소셜 회원이 아니라면
if(memberInfo && !memberInfo.social){
moveToPath("/")
}else {
moveToPath("/member/modify")
}
})
})
}, [authCode])
return (
Kakao Login Redirect
{authCode}
)
}
export default KakaoRedirectPage;
===========================================================
\src\pages\member\LoginPage.js
===========================================================
import LoginComponent from "../../components/member/LoginComponent";
import BasicMenu from "../../components/menus/BasicMenu";
const LoginPage = () => {
return (
);
}
export default LoginPage;
===========================================================
\src\pages\member\LogoutPage.js
===========================================================
import LogoutComponent from "../../components/member/LogoutComponent";
import BasicMenu from "../../components/menus/BasicMenu";
const LogoutPage = () => {
return (
);
}
export default LogoutPage;
===========================================================
\src\pages\member\ModifyPage.js
===========================================================
import ModifyComponent from "../../components/member/ModifyComponent";
import BasicLayout from "../../layouts/BasicLayout";
const ModfyPage = () => {
return (
Member Modify Page
);
}
export default ModfyPage;
===========================================================
\src\pages\products\AddPage.js
===========================================================
import AddComponent from "../../components/products/AddComponent";
const AddPage = () => {
return (
);
}
export default AddPage;
===========================================================
\src\pages\products\IndexPage.js
===========================================================
import { Outlet, useNavigate } from "react-router-dom";
import BasicLayout from "../../layouts/BasicLayout";
import { useCallback } from "react";
const IndexPage = () => {
const navigate = useNavigate()
const handleClickList = useCallback(() => {
navigate({ pathname:'list' })
})
const handleClickAdd = useCallback(() => {
navigate({ pathname:'add' })
})
return (
Products Menus
);
}
export default IndexPage;
===========================================================
\src\pages\products\ListPage.js
===========================================================
import ListComponent from "../../components/products/ListComponent";
const ListPage = () => {
return (
);
}
export default ListPage;
===========================================================
\src\pages\products\ModifyPage.js
===========================================================
import { useParams } from "react-router-dom";
import ModifyComponent from "../../components/products/ModifyComponent";
const ModifyPage = () => {
const {pno} = useParams()
return (
);
}
export default ModifyPage;
===========================================================
\src\pages\products\ReadPage.js
===========================================================
import { useParams } from "react-router-dom";
import ReadComponent from "../../components/products/ReadComponent";
const ReadPage = () => {
const {pno} = useParams()
return (
);
}
export default ReadPage;
===========================================================
\src\pages\todo\AddPage.js
===========================================================
import AddComponent from "../../components/todo/AddComponent";
const AddPage = () => {
return (
);
}
export default AddPage;
===========================================================
\src\pages\todo\IndexPage.js
===========================================================
import { Outlet, useNavigate } from "react-router-dom";
import BasicLayout from "../../layouts/BasicLayout";
import { useCallback } from "react";
const IndexPage = () => {
const navigate = useNavigate()
const handleClickList = useCallback(() => {
navigate({ pathname:'list' })
})
const handleClickAdd = useCallback(() => {
navigate({ pathname:'add' })
})
return (
);
}
export default IndexPage;
===========================================================
\src\pages\todo\ListPage.js
===========================================================
import ListComponent from "../../components/todo/ListComponent";
const ListPage = () => {
return (
);
}
export default ListPage;
===========================================================
\src\pages\todo\ModifyPage.js
===========================================================
import { useParams } from "react-router-dom";
import ModifyComponent from "../../components/todo/ModifyComponent";
const ModifyPage = () => {
const {tno} = useParams()
return (
);
}
export default ModifyPage;
===========================================================
\src\pages\todo\ReadPage.js
===========================================================
import { useParams } from "react-router-dom";
import ReadComponent from "../../components/todo/ReadComponent";
const ReadPage = () => {
const {tno} = useParams()
return (
Todo Read Page Component {tno}
);
}
export default ReadPage;
===========================================================
\src\reportWebVitals.js
===========================================================
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
===========================================================
\src\router\memberRouter.js
===========================================================
import { Suspense, lazy } from "react";
const Loading = Loading....
const Login = lazy(() => import("../pages/member/LoginPage"))
const LogoutPage = lazy(() => import("../pages/member/LogoutPage"))
const KakaoRedirect = lazy(() => import("../pages/member/KakaoRedirectPage"))
const MemberModify = lazy(() => import("../pages/member/ModifyPage"))
const memberRouter = () => {
return [
{
path:"login",
element:
},
{
path:"logout",
element: ,
},
{
path:"kakao",
element: ,
},
{
path:"modify",
element: ,
},
]
}
export default memberRouter
===========================================================
\src\router\productsRouter.js
===========================================================
import { Suspense, lazy } from "react";
import { Navigate } from "react-router-dom";
const Loading = Loading....
const ProductsList = lazy(() => import("../pages/products/ListPage"))
const ProductsAdd = lazy(() => import("../pages/products/AddPage"))
const ProductRead = lazy(() => import("../pages/products/ReadPage"))
const ProductModify = lazy(() => import("../pages/products/ModifyPage"))
const productsRouter = () => {
return [
{
path: "list",
element:
},
{
path: "",
element:
},
{
path: "add",
element:
},
{
path: "read/:pno",
element:
},
{
path: "modify/:pno",
element:
}
]
}
export default productsRouter;
===========================================================
\src\router\root.js
===========================================================
import { Suspense, lazy } from "react";
import todoRouter from "./todoRouter";
import productsRouter from "./productsRouter";
import memberRouter from "./memberRouter";
const { createBrowserRouter } = require("react-router-dom");
const Loading = Loading....
const Main = lazy(() => import("../pages/MainPage"))
const About = lazy(() => import("../pages/AboutPage"))
const TodoIndex = lazy(() => import("../pages/todo/IndexPage"))
const ProductsIndex = lazy(() => import("../pages/products/IndexPage"))
const root = createBrowserRouter([
{
path: "",
element:
},
{
path: "about",
element:
},
{
path: "todo",
element: ,
children: todoRouter()
},
{
path: "products",
element: ,
children: productsRouter()
},
{
path: "member",
children: memberRouter()
}
])
export default root;
===========================================================
\src\router\todoRouter.js
===========================================================
import { Suspense, lazy } from "react";
import { Navigate } from "react-router-dom";
const Loading = Loading....
const TodoList = lazy(() => import("../pages/todo/ListPage"))
const TodoRead = lazy(() => import("../pages/todo/ReadPage"))
const TodoAdd = lazy(() => import("../pages/todo/AddPage"))
const TodoModify = lazy(() => import("../pages/todo/ModifyPage"))
const todoRouter = () => {
return [
{
path: "list",
element:
},
{
path: "",
element:
},
{
path: "read/:tno",
element:
},
{
path: "add",
element:
},
{
path: "modify/:tno",
element:
}
]
}
export default todoRouter;
===========================================================
\src\setupTests.js
===========================================================
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
===========================================================
\src\slices\cartSlice.js
===========================================================
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { getCartItems, postChangeCart } from "../api/cartApi";
export const getCartItemsAsync = createAsyncThunk('getCartItemsAsync', () => {
return getCartItems()
})
export const postChangeCartAsync = createAsyncThunk('postCartItemsAsync', (param) => {
return postChangeCart(param)
})
const initState = []
const cartSlice = createSlice({
name: 'cartSlice',
initialState: initState,
extraReducers: (builder) => {
builder.addCase(
getCartItemsAsync.fulfilled, (state, action) => {
console.log("getCartItemsAsync fulfilled")
return action.payload
}
)
.addCase(
postChangeCartAsync.fulfilled, (state, action) => {
console.log("postCartItemsAsync fulfilled")
return action.payload
}
)
}
})
export default cartSlice.reducer
===========================================================
\src\slices\loginSlice.js
===========================================================
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { loginPost } from "../api/memberApi";
import { getCookie, setCookie, removeCookie } from "../util/cookieUtil";
const initState = {
email:''
}
export const loginPostAsync = createAsyncThunk('loginPostAsync', (param) => {
return loginPost(param)
})
const loadMemberCookie = () => { //쿠키에서 로그인 정보 로딩
const memberInfo = getCookie("member")
//닉네임 처리
if(memberInfo && memberInfo.nickname) {
memberInfo.nickname = decodeURIComponent(memberInfo.nickname)
}
return memberInfo
}
const loginSlice = createSlice({
name: 'LoginSlice',
initialState: loadMemberCookie()|| initState, //쿠키가 없다면 초깃값사용
reducers: {
login: (state, action) => {
console.log("login.....")
//{소셜로그인 회원이 사용}
const payload = action.payload
setCookie("member",JSON.stringify(payload), 1) //1일
return payload
},
logout: (state, action) => {
console.log("logout....")
removeCookie("member")
return {...initState}
}
},
extraReducers: (builder) => {
builder.addCase( loginPostAsync.fulfilled, (state, action) => {
console.log("fulfilled")
const payload = action.payload
//닉네임 한글 처리
// if(payload.nickname){
// payload.nickname = encodeURIComponent(payload.nickname)
// }
//정상적인 로그인시에만 저장
if(!payload.error){
setCookie("member",JSON.stringify(payload), 1) //1일
}
return payload
})
.addCase(loginPostAsync.pending, (state,action) => {
console.log("pending")
})
.addCase(loginPostAsync.rejected, (state,action) => {
console.log("rejected")
})
}
})
export const {login,logout} = loginSlice.actions
export default loginSlice.reducer
===========================================================
\src\store.js
===========================================================
import { configureStore } from '@reduxjs/toolkit'
import loginSlice from './slices/loginSlice'
import cartSlice from './slices/cartSlice'
export default configureStore({
reducer: {
"loginSlice": loginSlice,
"cartSlice" : cartSlice
}
})
===========================================================
\src\util\cookieUtil.js
===========================================================
import { Cookies } from "react-cookie";
const cookies = new Cookies()
export const setCookie = (name, value, days) => {
const expires = new Date()
expires.setUTCDate(expires.getUTCDate() + days ) //보관기한
return cookies.set(name, value, {path:'/', expires:expires})
}
export const getCookie = (name) => {
return cookies.get(name)
}
export const removeCookie = (name , path="/") => {
cookies.remove(name, {path} )
}
===========================================================
\src\util\jwtUtil.js
===========================================================
import axios from "axios";
import { getCookie, setCookie } from "./cookieUtil";
import { API_SERVER_HOST } from "../api/todoApi";
const jwtAxios = axios.create()
const refreshJWT = async (accessToken, refreshToken) => {
const host = API_SERVER_HOST
const header = {headers: {"Authorization":`Bearer ${accessToken}`}}
const res = await axios.get(`${host}/api/member/refresh?refreshToken=${refreshToken}`, header)
console.log("----------------------")
console.log(res.data)
return res.data
}
//before request
const beforeReq = (config) => {
console.log("before request.............")
const memberInfo = getCookie("member")
if( !memberInfo ) {
console.log("Member NOT FOUND")
return Promise.reject(
{response:
{data:
{error:"REQUIRE_LOGIN"}
}
}
)
}
const {accessToken} = memberInfo
// Authorization 헤더 처리
config.headers.Authorization = `Bearer ${accessToken}`
return config
}
//fail request
const requestFail = (err) => {
console.log("request error............")
return Promise.reject(err)
}
//before return response
const beforeRes = async (res) => {
console.log("before return response...........")
//console.log(res)
//'ERROR_ACCESS_TOKEN'
const data = res.data
if(data && data.error ==='ERROR_ACCESS_TOKEN'){
const memberCookieValue = getCookie("member")
const result = await refreshJWT( memberCookieValue.accessToken, memberCookieValue.refreshToken )
console.log("refreshJWT RESULT", result)
memberCookieValue.accessToken = result.accessToken
memberCookieValue.refreshToken = result.refreshToken
setCookie("member", JSON.stringify(memberCookieValue), 1)
//원래의 호출
const originalRequest = res.config
originalRequest.headers.Authorization = `Bearer ${result.accessToken}`
return await axios(originalRequest)
}
return res
}
//fail response
const responseFail = (err) => {
console.log("response fail error.............")
return Promise.reject(err);
}
jwtAxios.interceptors.request.use( beforeReq, requestFail )
jwtAxios.interceptors.response.use( beforeRes, responseFail)
export default jwtAxios