.gitignore package-lock.json package.json public README.md src tailwind.config.js =========================================================== \src\api\cartApi.js =========================================================== import {API_SERVER_HOST} from "./todoApi"; import jwtAxios from "../util/jwtUtil"; 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 =`15ad2f01de3b217534d2eb44217f51eb` //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 React from 'react'; import {API_SERVER_HOST} from "../../api/todoApi"; const host = API_SERVER_HOST function CartItemComponent({cino, pname, price, pno, qty, imageFile,changeCart, email}) { const handleClickQty = (amount) => { changeCart({email:email, cino:cino, pno: 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 (
    Loading.....
    ); } export default FetchingModal; =========================================================== \src\components\common\PageComponent.js =========================================================== const PageComponent = ({serverData, movePage,handleClickPage}) => { 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 (
    로그인시에 자동 가입처리 됩니다
    KAKAO LOGIN
    ) } 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 (
    Login Component
    Email
    Password
    ); } 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 (
    Logout Component
    ); } export default LogoutComponent; =========================================================== \src\components\member\ModifyComponent.js =========================================================== import { useEffect } from "react"; import { useState } from "react"; 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 {moveToLogin,loginState} = useCustomLogin() const loginInfo = loginState 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 (
    {result? :<>}
    Email
    Password
    Nickname
    ); } 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 addMutation = useMutation( { mutationFn: (product) => postAdd(product) } ) const queryClient = useQueryClient() const handleChangeProduct = (e) => { product[e.target.name] = e.target.value setProduct({...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) console.log(formData) // setFetching(true) // postAdd(formData).then(data=> { // setFetching(false) // setResult(data.result) // }) addMutation.mutate(formData) } const closeModal = () => { //ResultModal 종료 queryClient.invalidateQueries("products/list") moveToList({page:1}) //모달 창이 닫히면 이동 } return (
    {addMutation.isPending ? :<>} {addMutation.isSuccess? : <> }
    Product Name
    Desc
    Price
    Files
    ); } export default AddComponent; =========================================================== \src\components\products\ListComponent.js =========================================================== import { useEffect, useState } from "react"; 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, useQueryClient} from "@tanstack/react-query"; const host = API_SERVER_HOST const ListComponent = () => { const queryClient = useQueryClient() const {page, size, refresh, moveToList, moveToRead} = useCustomMove() const {exceptionHandle, moveToLoginReturn} = useCustomLogin() const handleClickPage = (pageParam) => { // if(pageParam.page === parseInt(page)){ // queryClient.invalidateQueries("products/list") // } moveToList(pageParam) } const {data, isFetching, error, isError} = useQuery({ queryKey: ['products/list', {page,size, refresh}], queryFn: () => getList({page,size}), staleTime: 1000 * 60 }) if(isError){ return moveToLoginReturn() } if(isFetching){ return } const serverData = data return (
    {serverData.dtoList.map(product =>
    moveToRead(product.pno)} >
    {product.pno}
    product
    이름: {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 { API_SERVER_HOST } from "../../api/todoApi"; import useCustomMove from "../../hooks/useCustomMove"; import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query"; import FetchingModal from "../common/FetchingModal"; 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 queryClient = useQueryClient() const delMutation = useMutation({mutationFn: (pno) => deleteOne(pno)}) const modMutation = useMutation({mutationFn: (product) => putOne(pno, product)}) //이동용 함수 const {moveToRead, moveToList} = useCustomMove() const [product, setProduct] = useState(initState) const [fetching, setFetching] = useState(false) const [result, setResult] = useState() const uploadRef = useRef() const query = useQuery({ queryKey: ['products', pno], queryFn: () => 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 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 handleClickDelete = () => { delMutation.mutate(pno) } const closeModal = () => { if(delMutation.isSuccess) { queryClient.invalidateQueries(['products', pno]) queryClient.invalidateQueries(['products/list']) moveToList() } if(modMutation.isSuccess) { queryClient.invalidateQueries(['products', pno]) queryClient.invalidateQueries(['products/list']) moveToRead(pno) } } return (
    {query.isFetching || delMutation.isPending || modMutation.isPending ? :<>} { delMutation.isSuccess || modMutation.isSuccess? : <> }
    Product Name
    Desc
    Price
    DELETE
    Files
    Images
    {product.uploadFileNames.map( (imgFile, i) =>
    img
    )}
    ); } export default ModifyComponent; =========================================================== \src\components\products\ReadComponent.js =========================================================== import {getOne} from "../../api/productsApi" import { API_SERVER_HOST } from "../../api/todoApi" import useCustomMove from "../../hooks/useCustomMove" import FetchingModal from "../common/FetchingModal" import useCustomCart from "../../hooks/useCustomCart"; import useCustomLogin from "../../hooks/useCustomLogin"; import {useQuery} from "@tanstack/react-query"; const host = API_SERVER_HOST const ReadComponent = ({pno}) => { //화면 이동용 함수 const {moveToList, moveToModify} = useCustomMove() //현재 사용자의 장바구니 아이템들 const {cartItems, changeCart} = useCustomCart() const {loginState} = useCustomLogin() const {data, isFetching} = useQuery({ queryKey: ['products', pno], queryFn: () => getOne(pno), staleTime: 1000 * 10 }) 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, qty:qty, pno:pno}) } if(isFetching) { return } const product = data return (
    PNO
    {product.pno}
    PNAME
    {product.pname}
    PRICE
    {product.price}
    PDESC
    {product.pdesc}
    {product.uploadFileNames.map( (imgFile, i) => product )}
    ) } 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 ? : <>}
    TITLE
    WRITER
    DUEDATE
    ); } 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 ? :<>}
    TNO
    {todo.tno}
    WRITER
    {todo.writer}
    TITLE
    DUEDATE
    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) =>
    {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( { mutationFn: (param) => postChangeCart(param), onSuccess: (result) => setCartItems(result) }) const query = useQuery({ queryKey: ["cart"], queryFn: 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 {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 { useDispatch } from "react-redux"; import { login } from "../../slices/loginSlice"; 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 (
    Products Add Page
    ); } 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
    LIST
    ADD
    ); } export default IndexPage; =========================================================== \src\pages\products\ListPage.js =========================================================== import ListComponent from "../../components/products/ListComponent"; const ListPage = () => { return (
    Products List Page
    ); } 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 (
    Products Modify Page
    ); } 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 (
    Products Read Page
    ); } export default ReadPage; =========================================================== \src\pages\todo\AddPage.js =========================================================== import AddComponent from "../../components/todo/AddComponent"; const AddPage = () => { return (
    Todo Add Page
    ); } 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 (
    LIST
    ADD
    ); } export default IndexPage; =========================================================== \src\pages\todo\ListPage.js =========================================================== import ListComponent from "../../components/todo/ListComponent"; const ListPage = () => { return (
    Todo List Page Component
    ); } 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 (
    Todo Modify Page
    ); } 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('postChangeCartAsync', (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") console.log(action.payload) if(action.payload.error){ } return action.payload }) .addCase(postChangeCartAsync.fulfilled, (state, action) => { console.log("postChangeCartAsync.fulfilled") console.log(action) 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