.eslintrc.cjs .gitignore index.html package-lock.json package.json postcss.config.js public README.md src tailwind.config.js tsconfig.json tsconfig.node.json vite.config.ts =========================================================== \src\apis\cartApi.ts =========================================================== import { CartItem, CartItemList } from "../interfaces/Cart" import jwtAxios from "../util/jwtUtil" import { API_SERVER_HOST } from "./todoApi" const host = `${API_SERVER_HOST}/api/cart` export const getCartItems = async ( ): Promise => { const res = await jwtAxios.get(`${host}/items`) return res.data } export const postChangeCart = async (cartItem:CartItem): Promise<[CartItemList]> => { const res = await jwtAxios.post(`${host}/change`, cartItem) return res.data } =========================================================== \src\apis\kakaoApi.ts =========================================================== import axios from "axios" import { API_SERVER_HOST } from "./todoApi"; import { Member, MemberModifyType } from "../interfaces/Member"; const rest_api_key =`6b8ecbb1a3cdd9c087161e35f0309afb` //REST키값 const redirect_uri =`http://localhost:5173/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 = ():string => { const kakaoURL = `${auth_code_path}?client_id=${rest_api_key}&redirect_uri=${redirect_uri}&response_type=code`; return kakaoURL } export const getAccessToken = async (authCode:string): Promise => { 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:string) : Promise => { const res = await axios.get(`${API_SERVER_HOST}/api/member/kakao?accessToken=${accessToken}`) return res.data } =========================================================== \src\apis\memberApi.ts =========================================================== import axios from "axios" import { API_SERVER_HOST, UpdateDeleteResultType } from "./todoApi" import { Member, MemberModifyType } from "../interfaces/Member" import jwtAxios from "../util/jwtUtil" const host = `${API_SERVER_HOST}/api/member` export type TLoginParam = { email:string, pw:string } export const loginPost = async (loginParam:TLoginParam): Promise => { 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:MemberModifyType): Promise=> { const res = await jwtAxios.put(`${host}/modify`, member) return res.data } =========================================================== \src\apis\productsApi.ts =========================================================== import { Product } from "../interfaces/Product"; import { API_SERVER_HOST } from "./todoApi"; import { PageParam } from "../interfaces/PageParam"; import { PageResponse } from "../interfaces/PageResponse"; //import axios from "axios"; import jwtAxios from "../util/jwtUtil"; const host = `${API_SERVER_HOST}/api/products` export type AddResultType = { result: number } export type UpdateDeleteResultType = { result: string } export const postAdd = async (product: FormData): Promise => { const res = await jwtAxios.post(`${host}/` , product) return res.data } export const getList = async ( pageParam: PageParam ) : Promise> => { const {page,size} = pageParam const res = await jwtAxios.get(`${host}/list`, {params: {page:page,size:size }}) return res.data } export const getOne = async (pno : number) : Promise => { const res = await jwtAxios.get(`${host}/${pno}` ) return res.data } export const putOne = async (pno: number, product:FormData) : Promise => { 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:number) : Promise => { const res = await jwtAxios.delete(`${host}/${pno}`) return res.data } =========================================================== \src\apis\todoApi.ts =========================================================== //import axios, { AxiosResponse } from "axios" import { Todo, TodoInputType } from "../interfaces/Todo" import { PageResponse } from "../interfaces/PageResponse" import jwtAxios from "../util/jwtUtil" export const API_SERVER_HOST = 'http://localhost:8080' export type AddResultType = { TNO: number } export type UpdateDeleteResultType = { result: string } const prefix = `${API_SERVER_HOST}/api/todo` export const getOne = async (tno:number) : Promise => { const res = await jwtAxios.get(`${prefix}/${tno}` ) return res.data } export const getList = async ( pageParam: {page:number, size:number} ) : Promise> => { const {page,size} = pageParam const res = await jwtAxios.get(`${prefix}/list`, {params: {page:page,size:size }}) return res.data } export const postAdd = async (todoObj:TodoInputType): Promise => { const res = await jwtAxios.post(`${prefix}/` , todoObj) return res.data } export const deleteOne = async (tno:number) : Promise => { const res = await jwtAxios.delete(`${prefix}/${tno}` ) return res.data } export const putOne = async (todo:Todo) : Promise => { const res = await jwtAxios.put(`${prefix}/${todo.tno}`, todo) return res.data } =========================================================== \src\App.css =========================================================== #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); } @keyframes logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: no-preference) { a:nth-of-type(2) .logo { animation: logo-spin infinite 20s linear; } } .card { padding: 2em; } .read-the-docs { color: #888; } =========================================================== \src\App.tsx =========================================================== import {RouterProvider} from "react-router-dom"; import root from "./router/root.tsx"; 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\assets\react.svg =========================================================== =========================================================== \src\atoms\cartState.ts =========================================================== import { RecoilState, RecoilValueReadOnly, atom, selector } from "recoil"; import { CartItemList } from "../interfaces/Cart"; const initState: CartItemList[] = [] export const cartState :RecoilState = atom({ key:'cartState', default:initState }) export const cartTotalState: RecoilValueReadOnly = 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.ts =========================================================== import { RecoilState, atom } from "recoil"; import { Member } from "../interfaces/Member"; import { getCookie } from "../util/cookieUtil"; const initState: Member = { email:'' } const loadMemberCookie = (): Member => { //쿠키에서 체크 const memberInfo = getCookie("member") //닉네임 처리 if(memberInfo && memberInfo.nickname) { memberInfo.nickname = decodeURIComponent(memberInfo.nickname) } return memberInfo } const signinState: RecoilState = atom({ key:'signinState', default: loadMemberCookie() || initState }) export default signinState =========================================================== \src\components\cart\CartItemComponent.tsx =========================================================== import { API_SERVER_HOST } from "../../apis/todoApi"; import { CartItem, CartItemList } from "../../interfaces/Cart"; const host = API_SERVER_HOST interface CartItemListProps extends CartItemList { changeCart: (item: CartItem) => void email: string } const CartItemComponent = ({cino, pname, price, pno, qty, imageFile, changeCart, email}: CartItemListProps) => { const handleClickQty = (amount:number) => { changeCart({email, cino, pno, qty: qty + amount}) } return (
  • Cart Item No: {cino}
    Pno: {pno}
    Name: {pname}
    Price: {price}
    Qty: {qty}
    { (qty ?? 0) * (price ?? 0) } 원
  • ); } export default CartItemComponent; =========================================================== \src\components\commons\FetchingModal.tsx =========================================================== const FetchingModal:React.FC<{}> = ( ) => { return (
    Loading.....
    ); } export default FetchingModal; =========================================================== \src\components\commons\PageComponent.tsx =========================================================== import * as React from 'react'; import { PageResponse } from '../../interfaces/PageResponse'; import { Todo } from '../../interfaces/Todo'; interface PageProps { serverData: PageResponse movePage: Function } const PageComponent: React.FC> = ({serverData, movePage}: PageProps<{}>) => { 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\commons\ResultModal.tsx =========================================================== import * as React from 'react'; interface ResultModalProps { title: string, content: string, callbackFn: () => void } const ResultModal: React.FunctionComponent = ({title,content,callbackFn}) => { return (
    { if(callbackFn) { callbackFn() } }}>
    {title}
    {content}
    ); } export default ResultModal; =========================================================== \src\components\member\KakaoLoginComponent.tsx =========================================================== import React from 'react'; import { getKakaoLoginLink } from '../../apis/kakaoApi'; import { Link } from 'react-router-dom'; const KakaoLoginComponent: React.FC = () => { const link: string = getKakaoLoginLink() return (
    로그인시에 자동 가입처리 됩니다
    KAKAO LOGIN
    ) } export default KakaoLoginComponent; =========================================================== \src\components\member\LoginComponent.tsx =========================================================== import React from 'react'; import useCustomLogin, { TCusotmLogin } from '../../hooks/useCustomLogin'; import KakaoLoginComponent from './KakaoLoginComponent'; interface LoginCredentials { email: string; pw: string; } const LoginComponent: React.FC = () => { const [loginParams, setLoginParams] = React.useState({ email: '', pw: '' }); const {doLogin, moveToPath}:TCusotmLogin = useCustomLogin() const handleChange = (e: React.ChangeEvent) => { if (e.target.name in loginParams) { loginParams[e.target.name as keyof LoginCredentials] = e.target.value; } setLoginParams({ ...loginParams }); } const handleClickLogin = () => { //dispatch({type:'LoginSlice/login', payload: loginParams}) doLogin(loginParams) .then(data => { console.log(data) if(data.error) { alert("이메일과 패스워드를 다시 확인하세요") }else { alert("로그인 성공") moveToPath('/') } }) } return (
    Login Component
    Email
    Password
    ); }; export default LoginComponent; =========================================================== \src\components\member\LogoutComponent.tsx =========================================================== import useCustomLogin, { TCusotmLogin } from "../../hooks/useCustomLogin"; import { removeCookie} from "../../util/cookieUtil"; const LogoutComponent: React.FC<{}> = () => { const {doLogout, moveToPath}:TCusotmLogin = useCustomLogin() const handleClickLogout = () => { doLogout() alert("로그아웃되었습니다.") removeCookie("member") moveToPath("/") } return (
    Logout Component
    ); } export default LogoutComponent; =========================================================== \src\components\member\ModifyComponent.tsx =========================================================== import { ChangeEvent, useEffect, useState } from "react"; import { useSelector } from "react-redux"; import { RootState } from "../../store"; import { Member, MemberModifyType } from "../../interfaces/Member"; import { modifyMember } from "../../apis/memberApi"; import useCustomLogin, { TCusotmLogin } from "../../hooks/useCustomLogin"; import ResultModal from "../commons/ResultModal"; const initState: MemberModifyType = { email: '', pw:'', nickname:'' } const ModifyComponent: React.FC = () => { const [member, setMember] = useState (initState) const loginInfo: Member = useSelector( (state: RootState) => state.loginSlice) const {moveToLogin}:TCusotmLogin = useCustomLogin() const [result, setResult] = useState() useEffect(() => { setMember({...loginInfo, pw:'ABCD'}) },[loginInfo]) const handleChange = (e:ChangeEvent) => { const prop:string = e.target.name const value:string = e.target.value if (prop === 'pw') { member.pw = value }else if(prop === 'nickname '){ member.nickname = 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.tsx =========================================================== import { useSelector } from "react-redux"; import { Link } from "react-router-dom"; import { RootState } from "../../store"; import { TLoginParam } from "../../apis/memberApi"; import useCustomLogin from "../../hooks/useCustomLogin"; const BasicMenu = () => { const loginState = useCustomLogin().loginState //console.log(loginState) return ( ); } export default BasicMenu; =========================================================== \src\components\menus\CartComponent.tsx =========================================================== import useCustomLogin, { TCusotmLogin } from "../../hooks/useCustomLogin"; import { useEffect, useMemo } from "react"; import useCustomCart, { TCusotmCart } from "../../hooks/useCustomCart"; import CartItemComponent from "../cart/CartItemComponent"; import { useRecoilValue } from "recoil"; import { cartTotalState } from "../../atoms/cartState"; const CartComponent: React.FC = () => { const {loginState}:TCusotmLogin = useCustomLogin() const {cartItems, changeCart }: TCusotmCart = useCustomCart() const totalValue = useRecoilValue(cartTotalState) return (
    {loginState.email ?
    {loginState.nickname}'s Cart
    {cartItems.length}
      {cartItems.map( item => )}
    TOTAL: {totalValue}
    : <> }
    ); } export default CartComponent; =========================================================== \src\components\products\AddComponent.tsx =========================================================== import React from 'react'; import { AddResultType, postAdd } from '../../apis/productsApi'; import FetchingModal from '../commons/FetchingModal'; import ResultModal from '../commons/ResultModal'; import useCustomMove, { TCustomMove } from '../../hooks/useCustomMove'; import { QueryClient, useMutation, useQueryClient } from '@tanstack/react-query'; interface AddComponentProps { pname: string; pdesc: string; price: number; } const AddComponent: React.FC<{}> = () => { const [product, setProduct] = React.useState({ pname: '', pdesc: '', price: 0 }); const uploadRef = React.useRef(null); const {moveToList}:TCustomMove = useCustomMove() const handleChangeProduct = (e: React.ChangeEvent): void => { const { name, value } = e.target; setProduct({ ...product, [name]: value }); }; const addMutation = useMutation({mutationFn: postAdd}) const handleClickAdd = (): void => { const files = uploadRef.current?.files; const formData = new FormData(); if(files){ for(let i = 0; i < files.length; i++){ formData.append('files', files[i]); } } formData.append('pname', product.pname); formData.append('pdesc', product.pdesc); formData.append('price', product.price.toString()); console.log(formData); addMutation.mutate( formData ) //기존 코드에서 변경 } const queryClient: QueryClient = useQueryClient() const closeModal = () => { //ResultModal 종료 queryClient.invalidateQueries({queryKey:["products/list"]}) moveToList() } return (
    {addMutation.isPending? :<>} {addMutation.isSuccess? : <> }
    Product Name
    Desc
    Price
    Files
    ); }; export default AddComponent; =========================================================== \src\components\products\ListComponent.tsx =========================================================== import React from 'react'; import useCustomMove, { TCustomMove } from '../../hooks/useCustomMove'; import { PageResponse } from '../../interfaces/PageResponse'; import { Product } from '../../interfaces/Product'; import { getList } from '../../apis/productsApi'; import FetchingModal from '../commons/FetchingModal'; import { API_SERVER_HOST } from "../../apis/todoApi"; import PageComponent from '../commons/PageComponent'; import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query'; import { Navigate } from 'react-router-dom'; import { PageParam } from '../../interfaces/PageParam'; const host = API_SERVER_HOST const ListComponent: React.FC> = ()=> { const {page,size,refresh, moveToList, moveToRead, exceptionHandle }:TCustomMove = useCustomMove() const {isFetching, data, error, isError} = useQuery>({ queryKey: ['products/list', {page, size, refresh}], queryFn: () => getList({page, size}) , staleTime: 1000 * 5 }) const serverData = data if(isError) { console.log(error) return } //const queryClient: QueryClient = useQueryClient() //리액트 쿼리 초기화를 위한 현재 객체 const handleClickPage = (pageParam: PageParam) => { // if(pageParam.page === page){ // queryClient.invalidateQueries({queryKey:["products/list"]}) // } moveToList(pageParam) } if(!serverData) { return <> } return (

    Products List Component

    {isFetching? :<>}
    {serverData.dtoList.map(product =>
    moveToRead(product.pno)} >
    {product.pno}
    product
    이름: {product.pname}
    가격: {product.price}
    )}
    ); }; export default ListComponent; =========================================================== \src\components\products\ModifyComponent.tsx =========================================================== import React, { useEffect } from 'react'; import { Product } from '../../interfaces/Product'; import { UpdateDeleteResultType, deleteOne, getOne, putOne } from '../../apis/productsApi'; import FetchingModal from '../commons/FetchingModal'; import { API_SERVER_HOST } from '../../apis/todoApi'; import useCustomMove, { TCustomMove } from '../../hooks/useCustomMove'; import ResultModal from '../commons/ResultModal'; import { UseMutateFunction, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; interface Pno { pno:number } const host = API_SERVER_HOST const ModifyComponent: React.FC = ({pno}:Pno) => { const queryClient = useQueryClient() const delMutation = useMutation( {mutationFn: (pno: number) => deleteOne(pno) }) const modMutation = useMutation( {mutationFn: (product: FormData) => putOne( pno, product)} ) const [product, setProduct] = React.useState({ pno: 0, pname: '', price: 0, pdesc: '', delFlag: false, uploadFileNames: null }) //이동용 함수 const {moveToRead, moveToList}:TCustomMove = useCustomMove() const [result, setResult] = React.useState('') const uploadRef = React.useRef(null); 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: React.ChangeEvent): void => { const { name, value } = e.target; setProduct({ ...product, [name]: value }); }; const deleteOldImages = (imageName:string) => { const resultFileNames:string[]|undefined = product.uploadFileNames?.filter( fileName => fileName !== imageName) if(resultFileNames){ product.uploadFileNames = [...resultFileNames] } setProduct({...product}) } const handleClickModify = () => { const files = uploadRef.current?.files const formData = new FormData() if(files){ 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.toString()) formData.append("delFlag", product.delFlag.toString()) if(product.uploadFileNames){ 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 = () => { queryClient.invalidateQueries({queryKey: ['products', pno]}) queryClient.invalidateQueries({queryKey: ['products/list'] }) if(delMutation.isSuccess) { moveToList() } if(modMutation.isSuccess) { moveToRead(pno) } } return (
    Product Modify Component {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.tsx =========================================================== import React, { useEffect } from 'react'; import { Product } from '../../interfaces/Product'; import useCustomMove, { TCustomMove } from '../../hooks/useCustomMove'; import { getOne } from '../../apis/productsApi'; import FetchingModal from '../commons/FetchingModal'; import { API_SERVER_HOST } from '../../apis/todoApi'; import useCustomCart, { TCusotmCart } from '../../hooks/useCustomCart'; import useCustomLogin, { TCusotmLogin } from '../../hooks/useCustomLogin'; import { useQuery } from '@tanstack/react-query'; interface Pno { pno:number } const host = API_SERVER_HOST const ReadComponent: React.FunctionComponent = ({pno}:Pno):JSX.Element => { //로그인 정보 const {loginState}: TCusotmLogin = useCustomLogin() const {moveToList, moveToModify}:TCustomMove = useCustomMove() const [fetching, setFetching] = React.useState(false) //장바구니 기능 const {changeCart, cartItems}:TCusotmCart = useCustomCart() const handleClickAddCart = () => { let qty = 1 const addedItem = cartItems.filter(item => item.pno === pno)[0] if(addedItem) { if(window.confirm("이미 추가된 상품입니다. 추가하시겠습니까? ") === false) { return } qty = addedItem.qty + 1 } changeCart({email: loginState.email, pno:pno, qty:qty}) } const {isFetching, data} = useQuery({ queryKey: ['product', pno], queryFn: () => getOne(pno), staleTime: 1000 * 10 }) const product = data if(!product) { return <> } return (
    {isFetching ? : <>}
    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.tsx =========================================================== import { useState } from "react" import { AddResultType, postAdd } from "../../apis/todoApi"; import ResultModal from "../commons/ResultModal"; import useCustomMove, { TCustomMove } from "../../hooks/useCustomMove"; import { Todo, TodoInputType } from "../../interfaces/Todo"; const AddComponent : React.FC> = ()=> { const [todo, setTodo] = useState({ title: '', writer: '', dueDate: '' }) const [result, setResult] = useState() const {moveToList}:TCustomMove = useCustomMove() const handleChangeTodo = (e:React.ChangeEvent ):void => { if (e.target.name in todo) { todo[e.target.name as keyof TodoInputType] = e.target.value; } setTodo({ ...todo }); } const handleClickAdd = () : void => { console.log(todo) //console.log(todo) postAdd(todo) .then(data => { console.log(data) setResult(data) setTodo({ title: '', writer: '', dueDate: '' }) }).catch(e => { console.error(e) }) } const closeModal = (): void => { setResult(null) moveToList() } return (
    {/* 모달 처리 */} {result ? : <>}
    TITLE
    WRITER
    DUEDATE
    ) } export default AddComponent =========================================================== \src\components\todo\ListComponent.tsx =========================================================== import * as React from 'react'; import useCustomMove, { TCustomMove } from '../../hooks/useCustomMove'; import { PageResponse } from '../../interfaces/PageResponse'; import { Todo } from '../../interfaces/Todo'; import { getList } from '../../apis/todoApi'; import PageComponent from '../commons/PageComponent'; const ListComponent : React.FC> = ()=> { const {page,size, moveToList, refresh, moveToRead, exceptionHandle}:TCustomMove = useCustomMove() const [serverData, setServerData] = React.useState>() React.useEffect(() => { getList({page,size}).then( (data: PageResponse) => { setServerData(data) }).catch( (error) => { console.log("----------------------EEEEEE") }) },[page,size, refresh]) if(!serverData) { return <> } return (
    {serverData.dtoList.map(todo =>
    moveToRead(todo.tno)} //이벤트 처리 추가 >
    {todo.tno}
    {todo.title}
    {todo.dueDate}
    )}
    ) ; }; export default ListComponent; =========================================================== \src\components\todo\ModifyComponent.tsx =========================================================== import { useEffect, useState } from "react"; import { Todo } from "../../interfaces/Todo"; import { UpdateDeleteResultType, deleteOne, getOne, putOne } from "../../apis/todoApi"; import useCustomMove, { TCustomMove } from "../../hooks/useCustomMove"; import ResultModal from "../commons/ResultModal"; const initState = { tno:0, title:'', writer: '', dueDate: '', complete: false } interface Tno { tno:number } const ModifyComponent = ({tno}:Tno) => { const [todo, setTodo] = useState({...initState}) //모달 창을 위한 상태 const [result, setResult] = useState() //이동을 위한 기능들 const {moveToList, moveToRead}: TCustomMove = useCustomMove() useEffect(() => { getOne(tno).then(data => setTodo(data)) },[tno]) const handleChangeTodo = (e:React.ChangeEvent) => { const prop:string = e.target.name const value:any = e.target.value if (prop === 'title') { todo.title = value }else if(prop === 'dueDate'){ todo.dueDate = value } setTodo({...todo}) } const handleChangeTodoComplete = (e:React.ChangeEvent) => { const value = e.target.value todo.complete = (value === 'Y') setTodo({...todo}) } const handleClickModify = () => { //버튼 클릭시 putOne(todo).then(data => { console.log("modify result: " + data) setResult({result:'Modified'}) }) } const handleClickDelete = () => { //버튼 클릭시 deleteOne(tno).then( data => { console.log("delete result: " + data) setResult({result:'Deleted'}) }) } //모달 창이 close될때 const closeModal = () => { if(result?.result ==='Deleted') { moveToList() }else { moveToRead(tno) } } return (
    {result ? :<>}
    TNO
    {todo.tno}
    WRITER
    {todo.writer}
    TITLE
    DUEDATE
    COMPLETE
    ); } export default ModifyComponent; =========================================================== \src\components\todo\ReadComponent.tsx =========================================================== import * as React from 'react'; import { getOne } from '../../apis/todoApi'; import {Todo} from '../../interfaces/Todo' import useCustomMove, {TCustomMove} from '../../hooks/useCustomMove'; interface Tno { tno:number } const ReadComponent: React.FunctionComponent = ({tno}:Tno):JSX.Element => { const [todo, setTodo] = React.useState() const {moveToList, moveToModify}: TCustomMove = useCustomMove() React.useEffect(() => { getOne(tno).then( (data:Todo) => { console.log(data) setTodo(data) }) },[tno]) if(!todo) { return <> } 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:string, value: string |number) : JSX.Element =>
    {title}
    {value}
    export default ReadComponent; =========================================================== \src\hooks\useCustomCart.ts =========================================================== import { useDispatch, useSelector } from "react-redux" import { getCartItemsAsync, postChangeCartAsync } from "../slices/cartSlice" import { AppDispatch, RootState } from "../store" import { CartItem, CartItemList } from "../interfaces/Cart" import { useRecoilState } from "recoil" import { cartState } from "../atoms/cartState" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { getCartItems, postChangeCart } from "../apis/cartApi" import { useEffect } from "react" export interface TCusotmCart { cartItems: CartItemList[], changeCart: (param:CartItem) => void } const useCustomCart = () => { const [cartItems,setCartItems] = useRecoilState(cartState) const queryClient = useQueryClient() const changeMutation = useMutation({ mutationFn: ( (param:CartItem) => postChangeCart(param)), onSuccess: ( (result: CartItemList[] ) => setCartItems(result) ) }) const query = useQuery({ queryKey: ["cart"], queryFn: getCartItems, staleTime: 1000 * 60 * 60 }) // 1 hour useEffect(() => { if(query.isSuccess || changeMutation.isSuccess) { queryClient.invalidateQueries({queryKey: ["cart"]}) if(query.data){ setCartItems(query.data) } } },[query.isSuccess, query.data]) const changeCart = (param:CartItem) => { changeMutation.mutate(param) } return {cartItems, changeCart} } export default useCustomCart =========================================================== \src\hooks\useCustomLogin.ts =========================================================== import { useDispatch, useSelector } from "react-redux" import { createSearchParams, useNavigate } from "react-router-dom" import { loginPostAsync, logout } from "../slices/loginSlice" import { AppDispatch, RootState } from "../store" import { TLoginParam, loginPost } from "../apis/memberApi" import { Member } from "../interfaces/Member" import { useRecoilState, useResetRecoilState } from "recoil" import signinState from "../atoms/signinState" import { removeCookie, setCookie } from "../util/cookieUtil" import { cartState } from "../atoms/cartState" export interface TCusotmLogin { loginState: Member, doLogin: (loginParam:TLoginParam) => Promise doLogout: () => void, moveToPath: (path:string) => void, moveToLogin: () => void saveAsCookie: (data:Member) => void } const useCustomLogin = ( ): TCusotmLogin=> { const navigate = useNavigate() const [loginState, setLoginState] = useRecoilState(signinState) const resetState = useResetRecoilState(signinState) const resetCartState = useResetRecoilState(cartState) //장바구니 비우기 const doLogin = async (loginParam:TLoginParam) => { //----------로그인 함수 const result = await loginPost(loginParam) console.log(result) saveAsCookie(result) return result } const saveAsCookie = (data:Member) => { setCookie("member",JSON.stringify(data), 1) //1일 setLoginState(data) } const doLogout = () => { //---------------로그아웃 함수 removeCookie('member') resetState() resetCartState() } const moveToPath = (path:string) => { //----------------페이지 이동 navigate({pathname: path}, {replace:true}) } const moveToLogin = () => { //----------------------로그인 페이지로 이동 navigate({pathname: '/member/login'}, {replace:true}) } return {loginState, doLogin, doLogout, moveToPath, moveToLogin, saveAsCookie} } export default useCustomLogin =========================================================== \src\hooks\useCustomMove.ts =========================================================== import { createSearchParams, useNavigate, useSearchParams } from "react-router-dom" import { PageParam } from "../interfaces/PageParam" import { useState } from "react" export interface TCustomMove { moveToList: (param?:PageParam) => void, moveToModify: (num:number) => void, moveToRead: (num:number) => void, page: number, size: number, refresh: boolean, exceptionHandle: (e:any) => void } const getNum = (param: string | null, defaultValue: number) : number => { if(!param){ return defaultValue } return parseInt(param) } const useCustomMove = () : TCustomMove => { const navigate = useNavigate() const [queryParams] = useSearchParams(); const [refresh, setRefresh] = useState(false) //추가 const page: number = getNum( queryParams.get('page'), 1) const size: number = getNum( queryParams.get('size'), 10) const queryDefault = createSearchParams({page: page.toString(), size: size.toString()}).toString() //새로 추가 const moveToList = (pageParam?: PageParam):void => { let queryStr = "" if(pageParam){ const pageNum = pageParam.page || 1 const sizeNum = pageParam.size || 10 console.log("-----------------") console.log(pageNum, sizeNum) queryStr = createSearchParams({page:pageNum.toString(), size: sizeNum.toString()}).toString() }else { queryStr = queryDefault } setRefresh(!refresh) //추가 navigate({pathname: `../list`,search:queryStr}) } const moveToModify = (num: number) : void => { console.log(queryDefault) navigate({ pathname: `../modify/${num}`, search: queryDefault //수정시에 기존의 쿼리 스트링 유지를 위해 }) } const moveToRead = (num:number) => { console.log(queryDefault) navigate({ pathname: `../read/${num}`, search: queryDefault }) } const exceptionHandle = (ex:any) => { 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 } } return {moveToList, page, size, moveToModify, refresh, moveToRead, exceptionHandle} } export default useCustomMove =========================================================== \src\index.css =========================================================== @tailwind base; @tailwind components; @tailwind utilities; =========================================================== \src\interfaces\Cart.ts =========================================================== export interface CartItem{ email?: string, pno: number, qty: number, cino?: number, } export interface CartItemList{ key?: number, cino: number, qty: number, pno: number, pname: string, price: number, imageFile: string } =========================================================== \src\interfaces\Member.ts =========================================================== export interface Member { email: string, pw?: string, nickname?:string social?: boolean, roleNames?: [string], accessToken?: string, refreshToken?: string, error?: MemberError } export interface MemberError { error?: string } export type MemberModifyType = Omit; =========================================================== \src\interfaces\PageParam.ts =========================================================== export interface PageParam { page: number , size? : number } =========================================================== \src\interfaces\PageResponse.ts =========================================================== export interface PageResponse { totalCount: number, prevPage: number, nextPage: number, totalPage: number, current: number prev:boolean next:boolean pageNumList:[number] dtoList:[T] } =========================================================== \src\interfaces\Product.ts =========================================================== export interface Product { pno: number, pname: string, price: number, pdesc: string, delFlag: boolean, uploadFileNames? : string[]|null } =========================================================== \src\interfaces\Todo.ts =========================================================== export interface Todo { tno: number, title: string, writer: string, complete: boolean, dueDate: string } export type TodoInputType = Omit; =========================================================== \src\layouts\BasicLayout.tsx =========================================================== import {PropsWithChildren} from 'react'; import BasicMenu from "../components/menus/BasicMenu.tsx"; import CartComponent from '../components/menus/CartComponent.tsx'; function BasicLayout({ children }: PropsWithChildren) { return ( <> {/* 기존 헤더 대신 BasicMenu*/ } {/* 상단 여백 my-5 제거 */}
    {/* 상단 여백 py-40 변경 flex 제거 */} {children}
    ); } export default BasicLayout; =========================================================== \src\main.tsx =========================================================== import ReactDOM from 'react-dom/client' import App from './App.tsx' import './index.css' import { Provider } from 'react-redux' import store from './store.ts' import { RecoilRoot } from 'recoil' ReactDOM.createRoot(document.getElementById('root')!).render( ) =========================================================== \src\pages\AboutPage.tsx =========================================================== import { Navigate } from "react-router-dom"; import useCustomLogin, { TCusotmLogin } from "../hooks/useCustomLogin.ts"; import BasicLayout from "../layouts/BasicLayout.tsx"; function AboutPage() { const {isLogin}:TCusotmLogin = useCustomLogin() if(!isLogin) { return } return (
    About Page
    ); } export default AboutPage; =========================================================== \src\pages\MainPage.tsx =========================================================== import BasicLayout from "../layouts/BasicLayout.tsx"; function MainPage() { return (
    Main Page
    ); } export default MainPage; =========================================================== \src\pages\member\KakaoRedirectPage.tsx =========================================================== import { useEffect } from "react"; import { useSearchParams } from "react-router-dom"; import { getAccessToken, getMemberWithAccessToken } from "../../apis/kakaoApi"; import { AppDispatch } from "../../store"; import useCustomLogin, { TCusotmLogin } from "../../hooks/useCustomLogin"; const KakaoRedirectPage = () => { const [searchParams] = useSearchParams() const {moveToPath, saveAsCookie}:TCusotmLogin = useCustomLogin() const authCode:string | null = searchParams.get("code") useEffect(() => { if(authCode === null) return 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.tsx =========================================================== import LoginComponent from "../../components/member/LoginComponent"; import BasicMenu from "../../components/menus/BasicMenu"; const LoginPage: React.FC = () => { return (
    ); } export default LoginPage; =========================================================== \src\pages\member\LogoutPage.tsx =========================================================== import LogoutComponent from "../../components/member/LogoutComponent"; import BasicMenu from "../../components/menus/BasicMenu"; const LogoutPage: React.FC<{}> = () => { return (
    ); } export default LogoutPage; =========================================================== \src\pages\member\ModifyPage.tsx =========================================================== import ModifyComponent from "../../components/member/ModifyComponent"; import BasicLayout from "../../layouts/BasicLayout"; const ModfyPage: React.FC<{}> = () => { return (
    Member Modify Page
    ); } export default ModfyPage; =========================================================== \src\pages\products\AddPage.tsx =========================================================== import React from 'react'; import AddComponent from '../../components/products/AddComponent'; const AddPage: React.FC = () => { return (
    Products Add Page
    ); }; export default AddPage; =========================================================== \src\pages\products\IndexPage.tsx =========================================================== import React, { useCallback } from 'react'; import { Outlet, useNavigate } from 'react-router-dom'; import BasicLayout from '../../layouts/BasicLayout'; const IndexPage: React.FC = () => { const navigate = useNavigate() const handleClickList = useCallback((): void => { navigate({ pathname:'list' }) },[]) const handleClickAdd = useCallback((): void => { navigate({ pathname:'add' }) },[]) return (
    Products Menus
    LIST
    ADD
    ); } export default IndexPage; =========================================================== \src\pages\products\ListPage.tsx =========================================================== import React from 'react'; import ListComponent from '../../components/products/ListComponent'; const ListPage: React.FC = () => { return (
    Products List Page
    ); }; export default ListPage; =========================================================== \src\pages\products\ModifyPage.tsx =========================================================== import React from 'react'; import { useParams } from 'react-router-dom'; import ModifyComponent from '../../components/products/ModifyComponent'; const ModifyPage: React.FC = () => { const {pno} = useParams<{pno:string}>() const pnoNumber: number = pno ? parseInt(pno) : 0 return (
    Products Modify Page
    ); }; export default ModifyPage; =========================================================== \src\pages\products\ReadPage.tsx =========================================================== import * as React from 'react'; import ReadComponent from '../../components/products/ReadComponent'; import { useParams } from 'react-router-dom'; const ReadPage: React.FC = () => { const {pno} = useParams<{ pno: string }>(); const pnoNumber: number = pno ? parseInt(pno) : 0 return (
    Products Read Page
    ) }; export default ReadPage; =========================================================== \src\pages\todo\AddPage.tsx =========================================================== import { useState } from "react"; import AddComponent from "../../components/todo/AddComponent"; import { Todo } from "../../interfaces/Todo"; function AddPage() { return (
    Todo Add Page
    ); } export default AddPage; =========================================================== \src\pages\todo\IndexPage.tsx =========================================================== import { Outlet, useNavigate} from "react-router-dom"; import BasicLayout from "../../layouts/BasicLayout"; const IndexPage:React.FC> = () => { const navigate = useNavigate() const handleClickList = () : void => { navigate({pathname:'list'}) } const handleClickAdd = () : void => { navigate({pathname:'add'}) } return (
    LIST
    ADD
    ); } export default IndexPage; =========================================================== \src\pages\todo\ListPage.tsx =========================================================== import ListComponent from "../../components/todo/ListComponent"; const ListPage:React.FC> = () => { return (
    Todo List Page Component
    ); } export default ListPage; =========================================================== \src\pages\todo\ModifyPage.tsx =========================================================== import { useParams } from "react-router-dom"; import ModifyComponent from "../../components/todo/ModifyComponent"; const ModifyPage: React.FC> = () => { const {tno} = useParams<{tno:string}>() const tnoNumber: number = tno ? parseInt(tno) : 0 return (
    Todo Modify Page
    ); } export default ModifyPage =========================================================== \src\pages\todo\ReadPage.tsx =========================================================== import { useParams } from "react-router-dom"; import ReadComponent from "../../components/todo/ReadComponent"; const ReadPage:React.FC> = () => { const {tno} = useParams<{ tno: string }>(); const tnoNumber: number = tno ? parseInt(tno) : 0 return (
    Todo Read Page Component {tno}
    ); } export default ReadPage =========================================================== \src\router\memberRouter.tsx =========================================================== import { ReactElement, Suspense, lazy } from "react"; const Loading =
    Loading....
    const Login = lazy(() => import("../pages/member/LoginPage.tsx")) const LogoutPage = lazy(() => import("../pages/member/LogoutPage.tsx")) const KakaoRedirect = lazy(() => import("../pages/member/KakaoRedirectPage.tsx")) const MemberModify = lazy(() => import("../pages/member/ModifyPage.tsx")) const memberRouter = ():Array<{path:string, element:ReactElement}> => { return [ { path:"login", element: }, { path:"logout", element: }, { path:"kakao", element: , }, { path:"modify", element: , } ] } export default memberRouter =========================================================== \src\router\productRouter.tsx =========================================================== import { ReactElement, Suspense, lazy } from 'react'; import { Navigate } from 'react-router-dom'; const Loading =
    Loading....
    const ProductsList = lazy(() => import("../pages/products/ListPage.tsx")) const ProductsAdd = lazy(() => import("../pages/products/AddPage.tsx")) const ProductRead = lazy(() => import("../pages/products/ReadPage.tsx")) const ProductModify = lazy(() => import("../pages/products/ModifyPage")) const productRouter = ():Array<{path:string, element:ReactElement}> => { return [ { path: "list", element: }, { path: "", element: }, { path: "add", element: }, { path: "read/:pno", element: }, { path: "modify/:pno", element: } ] } export default productRouter; =========================================================== \src\router\root.tsx =========================================================== import {createBrowserRouter} from "react-router-dom"; import {ComponentType, lazy, ReactNode, Suspense} from "react"; import todoRouter from "./todoRouter.tsx"; import productRouter from "./productRouter.tsx"; import memberRouter from "./memberRouter.tsx"; const Loading: ReactNode =
    Loading,,,,
    const Main:ComponentType = lazy(() => import('../pages/MainPage')) const About:ComponentType = lazy(() => import('../pages/AboutPage')) const TodoIndex: ComponentType = lazy(() => import("../pages/todo/IndexPage.tsx")) const ProductsIndex = lazy(() => import("../pages/products/IndexPage.tsx")) const root = createBrowserRouter([ { path: '', element:
    }, { path: 'about', element: }, { path: 'todo', element: , children: todoRouter() }, { path: 'products', element: , children: productRouter() }, { path: "member", children: memberRouter() } ]) export default root =========================================================== \src\router\todoRouter.tsx =========================================================== import {Suspense, lazy, ReactNode, ComponentType, ReactElement} from "react"; import {Navigate} from "react-router-dom"; const Loading:ReactNode =
    Loading....
    const TodoList:ComponentType = lazy(() => import("../pages/todo/ListPage")) const TodoRead:ComponentType = lazy(() => import("../pages/todo/ReadPage")) const TodoAdd:ComponentType = lazy(() => import("../pages/todo/AddPage")) const TodoModify:ComponentType = lazy(() => import("../pages/todo/ModifyPage")) const todoRouter = ():Array<{path:string, element:ReactElement}> => { return [ { path: "list", element: }, { path: "", element: }, { path: "read/:tno", element: }, { path: "modify/:tno", element: }, { path: "add", element: }, ] } export default todoRouter =========================================================== \src\slices\cartSlice.ts =========================================================== import { Slice, createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { getCartItems, postChangeCart } from "../apis/cartApi"; import { CartItem, CartItemList } from "../interfaces/Cart"; export const getCartItemsAsync = createAsyncThunk('getCartItemsAsync', () => { return getCartItems() }) export const postChangeCartAsync = createAsyncThunk('postCartItemsAsync', (param: CartItem) => { return postChangeCart(param) }) const initState: CartItemList[] = [] const cartSlice: Slice = createSlice({ name: 'cartSlice', initialState: initState, reducers: { }, 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.ts =========================================================== import { PayloadAction, Slice, createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { TLoginParam, loginPost } from "../apis/memberApi"; import { getCookie, removeCookie, setCookie } from "../util/cookieUtil"; import { Member } from "../interfaces/Member"; const loadMemberCookie = (): Member => { //쿠키에서 로그인 정보 로딩 const memberInfo = getCookie("member") //닉네임 처리 if(memberInfo && memberInfo.nickname) { memberInfo.nickname = decodeURIComponent(memberInfo.nickname) } return memberInfo } export const loginPostAsync = createAsyncThunk('loginPostAsync', (param:TLoginParam) => { return loginPost(param) }) const loginSlice: Slice = createSlice({ name: 'LoginSlice', initialState: loadMemberCookie()|| {email:''}, reducers: { login: (state, action:PayloadAction ) => { console.log("login.....") console.log(action) //{소셜로그인 회원이 사용} const payload = action.payload setCookie("member",JSON.stringify(payload), 1) //1일 return payload }, logout: ():Member => { console.log("logout....") removeCookie('member') return {email:''} } }, extraReducers: (builder) => { builder.addCase( loginPostAsync.fulfilled, (state,action) => { console.log("fulfilled") //console.log(action) const payload = action.payload //정상적인 로그인시에만 저장 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.ts =========================================================== import { configureStore } from '@reduxjs/toolkit' import loginSlice from './slices/loginSlice' import cartSlice from './slices/cartSlice' const store = configureStore({ reducer: { "loginSlice": loginSlice, "cartSlice": cartSlice, } }) export type RootState = ReturnType export type AppDispatch = typeof store.dispatch export default store =========================================================== \src\util\cookieUtil.ts =========================================================== import { Cookies } from "react-cookie"; const cookies:Cookies = new Cookies() export const setCookie = (name:string, value:string, days:number) => { const expires = new Date() expires.setUTCDate(expires.getUTCDate() + days ) //보관기한 return cookies.set(name, value, {path:'/', expires:expires}) } export const getCookie = (name:string) => { return cookies.get(name) } export const removeCookie = (name:string , path: string ="/") => { cookies.remove(name, {path} ) } =========================================================== \src\util\jwtUtil.ts =========================================================== import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from "axios"; import { getCookie, setCookie } from "./cookieUtil"; import { API_SERVER_HOST } from "../apis/todoApi"; const jwtAxios: AxiosInstance = axios.create() const refreshJWT = async (accessToken:string, refreshToken:string) => { 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: InternalAxiosRequestConfig): InternalAxiosRequestConfig| Promise=> { 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: AxiosError | Error): Promise => { console.log("request error............") return Promise.reject(err) } //before return response const beforeRes = async (res: AxiosResponse) : Promise => { console.log("before return response...........") 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: AxiosError | Error ) : Promise => { console.log("response fail error.............") return Promise.reject(err); } jwtAxios.interceptors.request.use( beforeReq, requestFail ) jwtAxios.interceptors.response.use( beforeRes, responseFail) export default jwtAxios =========================================================== \src\vite-env.d.ts =========================================================== ///