.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\productsApi.ts =========================================================== import axios from "axios"; import { Product } from "../interfaces/Product"; import { API_SERVER_HOST } from "./todoApi"; import { PageParam } from "../interfaces/PageParam"; import { PageResponse } from "../interfaces/PageResponse"; 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 axios.post(`${host}/` , product) return res.data } export const getList = async ( pageParam: PageParam ) : Promise> => { const {page,size} = pageParam const res = await axios.get(`${host}/list`, {params: {page:page,size:size }}) return res.data } export const getOne = async (pno : number) : Promise => { const res = await axios.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 axios.put(`${host}/${pno}`, product, header) return res.data } export const deleteOne = async (pno:number) : Promise => { const res = await axios.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" 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 axios.get(`${prefix}/${tno}` ) return res.data } export const getList = async ( pageParam: {page:number, size:number} ) : Promise> => { const {page,size} = pageParam const res = await axios.get(`${prefix}/list`, {params: {page:page,size:size }}) return res.data } export const postAdd = async (todoObj:TodoInputType): Promise => { const res = await axios.post(`${prefix}/` , todoObj) return res.data } export const deleteOne = async (tno:number) : Promise => { const res = await axios.delete(`${prefix}/${tno}` ) return res.data } export const putOne = async (todo:Todo) : Promise => { const res = await axios.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"; function App() { return ( ) } export default App =========================================================== \src\assets\react.svg =========================================================== =========================================================== \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\menus\BasicMenu.tsx =========================================================== import { Link } from "react-router-dom"; const BasicMenu = () => { return ( ); } export default BasicMenu; =========================================================== \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'; 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 [fetching, setFetching] = React.useState(false) const [result, setResult] = React.useState() const {moveToList}:TCustomMove = useCustomMove() const handleChangeProduct = (e: React.ChangeEvent): void => { const { name, value } = e.target; setProduct({ ...product, [name]: value }); }; 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); setFetching(true) postAdd(formData).then( data => { setFetching(false) setResult(data) }) } const closeModal = () => { //ResultModal 종료 setResult(null) moveToList() } return (
{fetching? :<>} {result? : <> }
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'; const host = API_SERVER_HOST const ListComponent: React.FC> = ()=> { const {page,size,refresh, moveToList, moveToRead }:TCustomMove = useCustomMove() const [serverData, setServerData] = React.useState>() //for FetchingModal const [fetching, setFetching] = React.useState(false) React.useEffect(() => { setFetching(true) getList({page,size}).then( (data: PageResponse) => { setServerData(data) setFetching(false) }) },[page,size, refresh]) if(!serverData) { return <> } return (

Products List Component

{fetching? :<>}
{serverData.dtoList.map(product =>
moveToRead(product.pno)} >
{product.pno}
product
이름: {product.pname}
가격: {product.price}
)}
); }; export default ListComponent; =========================================================== \src\components\products\ModifyComponent.tsx =========================================================== import React from 'react'; import { Product } from '../../interfaces/Product'; import { 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 { Result } from 'postcss'; import ResultModal from '../commons/ResultModal'; interface Pno { pno:number } const host = API_SERVER_HOST const ModifyComponent: React.FC = ({pno}:Pno) => { const [product, setProduct] = React.useState({ pno: 0, pname: '', price: 0, pdesc: '', delFlag: false, uploadFileNames: null }) const [fetching, setFetching] = React.useState(false) //이동용 함수 const {moveToRead, moveToList}:TCustomMove = useCustomMove() const [result, setResult] = React.useState('') const uploadRef = React.useRef(null); React.useEffect(() => { setFetching(true) getOne(pno).then(data => { setProduct(data) setFetching(false) }) },[pno]) 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]) } } 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 (
Product Modify Component {fetching ? : <>} {result? : <>}
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'; interface Pno { pno:number } const host = API_SERVER_HOST const ReadComponent: React.FunctionComponent = ({pno}:Pno):JSX.Element => { const [product, setProduct] = React.useState() const {moveToList, moveToModify}:TCustomMove = useCustomMove() const [fetching, setFetching] = React.useState(false) React.useEffect(() => { setFetching(true) getOne(pno).then( (data:Product) => { console.log(data) setProduct(data) setFetching(false) }) } ,[pno]) if(!product) { return <> } 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.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}:TCustomMove = useCustomMove() const [serverData, setServerData] = React.useState>() React.useEffect(() => { getList({page,size}).then( (data: PageResponse) => { setServerData(data) }) },[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\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 } 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 }) } return {moveToList, page, size, moveToModify, refresh, moveToRead} } export default useCustomMove =========================================================== \src\index.css =========================================================== @tailwind base; @tailwind components; @tailwind utilities; =========================================================== \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"; function BasicLayout({ children }: PropsWithChildren) { return ( <> {/* 기존 헤더 대신 BasicMenu*/ } {/* 상단 여백 my-5 제거 */}
{/* 상단 여백 py-40 변경 flex 제거 */} {children}
); } export default BasicLayout; =========================================================== \src\main.tsx =========================================================== import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')!).render( ) =========================================================== \src\pages\AboutPage.tsx =========================================================== import BasicLayout from "../layouts/BasicLayout.tsx"; function AboutPage() { 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\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\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"; 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() } ]) 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\vite-env.d.ts =========================================================== ///