.gitignore .idea node_modules package-lock.json package.json public README.md src tailwind.config.js =========================================================== \src\api\productsApi.js =========================================================== import axios from "axios" import { API_SERVER_HOST } from "./todoApi" const host = `${API_SERVER_HOST}/api/products` export const postAdd = async (product) => { const header = {headers: {"Content-Type": "multipart/form-data"}} // 경로 뒤 '/' 주의 const res = await axios.post(`${host}/`, product, header) return res.data } export const getList = async ( pageParam ) => { const {page,size} = pageParam const res = await axios.get(`${host}/list`, {params: {page:page,size:size }}) return res.data } export const getOne = async (tno) => { const res = await axios.get(`${host}/${tno}` ) return res.data } export const putOne = async (pno, product) => { const header = {headers: {"Content-Type": "multipart/form-data"}} const res = await axios.put(`${host}/${pno}`, product, header) return res.data } export const deleteOne = async (pno) => { const res = await axios.delete(`${host}/${pno}`) return res.data } =========================================================== \src\api\todoApi.js =========================================================== import axios from "axios" export const API_SERVER_HOST = 'http://localhost:8080' const prefix = `${API_SERVER_HOST}/api/todo` export const getOne = async (tno) => { const res = await axios.get(`${prefix}/${tno}` ) return res.data } export const getList = async ( pageParam ) => { const {page,size} = pageParam const res = await axios.get(`${prefix}/list`, {params: {page:page,size:size }}) return res.data } export const postAdd = async (todoObj) => { const res = await axios.post(`${prefix}/` , todoObj) return res.data } export const deleteOne = async (tno) => { const res = await axios.delete(`${prefix}/${tno}` ) return res.data } export const putOne = async (todo) => { const res = await axios.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"; 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\components\common\FetchingModal.js =========================================================== const FetchingModal = ( ) => { return (
Loading.....
); } export default FetchingModal; =========================================================== \src\components\common\PageComponent.js =========================================================== const PageComponent = ({serverData, movePage}) => { return (
{serverData.prev ?
movePage({page:serverData.prevPage} )}> Prev
: <>} {serverData.pageNumList.map(pageNum =>
movePage( {page:pageNum})}> {pageNum}
)} {serverData.next ?
movePage( {page:serverData.nextPage})}> Next
: <>}
); } export default PageComponent; =========================================================== \src\components\common\ResultModal.js =========================================================== const ResultModal = ( {title,content, callbackFn} ) => { return (
{ if(callbackFn) { callbackFn() } }}>
{title}
{content}
); } export default ResultModal; =========================================================== \src\components\menus\BasicMenu.js =========================================================== import { Link } from "react-router-dom"; const BasicMenu = () => { return ( ); } export default BasicMenu; =========================================================== \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"; const initState = { pname: '', pdesc: '', price: 0, files: [] } const AddComponent = () => { const [product,setProduct] = useState({...initState}) const uploadRef = useRef() const [fetching, setFetching] = useState(false) const [result, setResult] = useState(null) const {moveToList} = useCustomMove() //이동을 위한 함수 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) }) } const closeModal = () => { //ResultModal 종료 setResult(null) moveToList({page:1}) //모달 창이 닫히면 이동 } return (
{fetching? :<>} {result? : <> }
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"; const host = API_SERVER_HOST 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() //serverData는 나중에 사용 const [serverData, setServerData] = useState(initState) //for FetchingModal const [fetching, setFetching] = useState(false) useEffect(() => { setFetching(true) getList({page,size}).then(data => { console.log(data) setServerData(data) setFetching(false) }) }, [page,size, refresh]) return (
{fetching? :<>}
{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 { getOne, putOne, deleteOne } from "../../api/productsApi"; import FetchingModal from "../common/FetchingModal"; import { API_SERVER_HOST } from "../../api/todoApi"; import useCustomMove from "../../hooks/useCustomMove"; 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 [product, setProduct] = useState(initState) //결과 모달 const [result, setResult] = useState(null) //이동용 함수 const {moveToRead, moveToList} = useCustomMove() const [fetching, setFetching] = useState(false) const uploadRef = useRef() useEffect(() => { setFetching(true) getOne(pno).then(data => { setProduct(data) setFetching(false) } ) },[pno]) 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]) } //fetching setFetching(true) putOne(pno, formData).then(data => { //수정 처리 setResult('Modified') setFetching(false) }) } const handleClickDelete = () => { setFetching(true) deleteOne(pno).then(data => { setResult("Deleted") setFetching(false) }) } const closeModal = () => { if(result ==='Modified') { moveToRead(pno) }else if(result === 'Deleted') { moveToList({page:1}) } setResult(null) } return (
{fetching? :<>} {result? : <> }
Product Name
Desc
Price
DELETE
Files
Images
{product.uploadFileNames.map( (imgFile, i) =>
img
)}
); } export default ModifyComponent; =========================================================== \src\components\products\ReadComponent.js =========================================================== import { useEffect, useState } from "react" import {getOne} from "../../api/productsApi" import { API_SERVER_HOST } from "../../api/todoApi" import useCustomMove from "../../hooks/useCustomMove" import FetchingModal from "../common/FetchingModal" const initState = { pno:0, pname: '', pdesc: '', price: 0, uploadFileNames:[] } const host = API_SERVER_HOST const ReadComponent = ({pno }) => { const [product, setProduct] = useState(initState) //화면 이동용 함수 const {moveToList, moveToModify} = useCustomMove() //fetching const [fetching, setFetching] = useState(false) useEffect(() => { setFetching(true) getOne(pno).then(data => { setProduct(data) setFetching(false) }) }, [pno]) return (
{fetching? :<>}
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\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'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); =========================================================== \src\layouts\BasicLayout.js =========================================================== import BasicMenu from "../components/menus/BasicMenu"; const BasicLayout = ({children}) => { return ( <> {/* 기존 헤더 대신 BasicMenu*/ } {/* 상단 여백 my-5 제거 */}
{/* 상단 여백 py-40 변경 flex 제거 */} {children}
); } export default BasicLayout; =========================================================== \src\logo.svg =========================================================== =========================================================== \src\pages\AboutPage.js =========================================================== import BasicLayout from "../layouts/BasicLayout"; const AboutPage = () => { 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\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\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"; 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() } ]) 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';