https://zk202308a.github.io/reactbookcodes/
Heroic Features - Start Bootstrap Template
As always, Start Bootstrap has a powerful collectin of free templates.
zk202308a.github.io
화면 디자인 같은 부분은 강의에서 너무 빨리 넘어가고 안 보이는 경우도 있어서 해당 깃허브에서 코드 참고했음
1. 상품 관련 React-Router 설정
import React from "react";
function IndexPage() {
return <div>IndexPage</div>;
}
export default IndexPage;
mall\src\pages\products\IndexPage.js
import { Suspense, lazy } from "react";
const productsRouter = () => {
return [];
};
export default productsRouter;
mall\src\router\productsRouter.js
IndexPage와 productsRouter는 기본 구조만
import { Suspense, lazy } from "react";
import todoRouter from "./todoRouter";
import productsRouter from "./productsRouter";
const { createBrowserRouter } = require("react-router-dom");
const Loading = <div>Loading....</div>;
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: (
<Suspense fallback={Loading}>
<Main />
</Suspense>
),
},
{
path: "about",
element: (
<Suspense fallback={Loading}>
<About />
</Suspense>
),
},
{
path: "todo",
element: (
<Suspense fallback={Loading}>
<TodoIndex />
</Suspense>
),
children: todoRouter(),
},
{
path: "products",
element: (
<Suspense fallback={Loading}>
<ProductsIndex />
</Suspense>
),
children: productsRouter(),
},
]);
export default root;
mall\src\router\root.js
import React from "react";
import { Link } from "react-router-dom";
const BasicMenu = () => {
return (
<nav id="navbar" className=" flex bg-blue-300">
<div className="w-4/5 bg-gray-500">
<ul className="flex p-4 text-white font-bold">
<li className="pr-6 text-2xl">
<Link to={"/"}>Main</Link>
</li>
<li className="pr-6 text-2xl">
<Link to={"/about"}>About</Link>
</li>
<li className="pr-6 text-2xl">
<Link to={"/todo/"}>Todo</Link>
</li>
<li className="pr-6 text-2xl">
<Link to={"/products/"}>Products</Link>
</li>
</ul>
</div>
<div className="w-1/5 flex justify-end bg-orange-300 p-4 font-medium">
<div className="text-white text-sm m-1 rounded">Login</div>
</div>
</nav>
);
};
export default BasicMenu;
mall\src\components\menus\BasicMenu.js
Product의 라우팅 설정 부분 추가
2. 상품 목록 처리
import React from "react";
import BasicLayout from "../../layouts/BasicLayout";
import { Outlet } from "react-router-dom";
function IndexPage(props) {
return (
<BasicLayout>
<div className="text-black font-extrabold -mt-10">Products Menus</div>
<div className="w-full flex m-2 p-2 ">
<div className="text-xl m-1 p-2 w-20 font-extrabold text-center underline">
LIST
</div>
<div className="text-xl m-1 p-2 w-20 font-extrabold text-center underline">
ADD
</div>
</div>
<div className="flex flex-wrap w-full ">
<Outlet />
</div>
</BasicLayout>
);
}
export default IndexPage;
mall\src\pages\products\IndexPage.js
import React from "react";
import BasicLayout from "../../layouts/BasicLayout";
import { Outlet, useNavigate } from "react-router-dom";
function IndexPage(props) {
const navigate = useNavigate();
return (
<BasicLayout>
<div className="text-black font-extrabold -mt-10">Products Menus</div>
<div className="w-full flex m-2 p-2 ">
<div
className="text-xl m-1 p-2 w-20 font-extrabold text-center underline"
onClick={() => navigate("list")}
>
LIST
</div>
<div
className="text-xl m-1 p-2 w-20 font-extrabold text-center underline"
onClick={() => navigate("add")}
>
ADD
</div>
</div>
<div className="flex flex-wrap w-full ">
<Outlet />
</div>
</BasicLayout>
);
}
export default IndexPage;
그리고 IndexPage에 onClick 함수를 추가해서 add와 list 페이지로 넘어가는지 확인
import React from "react";
function ListPage(props) {
return (
<div className="p-4 w-full bg-white">
<div className="text-3xl font-extrabold">Products List Page</div>
</div>
);
}
export default ListPage;
mall\src\pages\products\ListPage.js
import { Suspense, lazy } from "react";
import { Navigate } from "react-router-dom";
const Loading = <div>Loading....</div>;
const ProductList = lazy(() => import("../pages/products/ListPage"));
const productsRouter = () => {
return [
{
path: "list",
element: (
<Suspense fallback={Loading}>
<ProductList />
</Suspense>
),
},
{
path: "",
element: <Navigate replace to={"/products/list"}></Navigate>,
},
];
};
export default productsRouter;
mall\src\router\productsRouter.js
메뉴에서 Products로 가면 바로 list로 가도록 설정
3. 상품 등록 컴포넌트 처리
import React from "react";
function AddPage(props) {
return (
<div className="p-4 w-full bg-white">
<div className="text-3xl font-extrabold">Products Add Page</div>
</div>
);
}
export default AddPage;
mall\src\pages\products\AddPage.js
import { lazy, Suspense } from "react";
import { Navigate } from "react-router-dom";
const Loading = <div>Loading....</div>;
const ProductList = lazy(() => import("../pages/products/ListPage"));
const ProductAdd = lazy(() => import("../pages/products/AddPage"));
const productsRouter = () => {
return [
{
path: "list",
element: (
<Suspense fallback={Loading}>
<ProductList></ProductList>
</Suspense>
),
},
{
path: "",
element: <Navigate replace to={"/products/list"}></Navigate>,
},
{
path: "add",
element: (
<Suspense fallback={Loading}>
<ProductAdd></ProductAdd>
</Suspense>
),
},
];
};
export default productsRouter;
mall\src\router\productsRouter.js
add 페이지로 가는 라우팅 설정도 완료
import React from "react";
import AddComponent from "../../components/products/AddComponent";
function AddPage(props) {
return (
<div className="p-4 w-full bg-white">
<div className="text-3xl font-extrabold">Products Add Page</div>
<AddComponent></AddComponent>
</div>
);
}
export default AddPage;
mall\src\pages\products\AddPage.js
먼저 AddPage 안에 AddComponent를 추가했다.
import React, { useRef, useState } from "react";
const initState = {
pname: "",
pdesc: "",
price: 0,
files: [],
};
function AddComponent(props) {
const [product, setProduct] = useState(initState);
const uploadRef = useRef();
const handleChangeProduct = (e) => {
product[e.target.name] = e.target.value;
setProduct({ ...product });
};
const handleClickAdd = (e) => {
console.log(product);
};
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Product Name</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="pname"
type={"text"}
value={product.pname}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Desc</div>
<textarea
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md resize-y"
name="pdesc"
rows="4"
onChange={handleChangeProduct}
value={product.pdesc}
>
{product.pdesc}
</textarea>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Price</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="price"
type={"number"}
value={product.price}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Files</div>
<input
ref={uploadRef}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
type={"file"}
multiple={true}
></input>
</div>
</div>
<div className="flex justify-end">
<div className="relative mb-4 flex p-4 flex-wrap items-stretch">
<button
type="button"
className="rounded p-4 w-36 bg-blue-500 text-xl text-white "
onClick={handleClickAdd}
>
ADD
</button>
</div>
</div>
</div>
);
}
export default AddComponent;
mall\src\components\products\AddComponent.js
uploadRef로 파일 업로드를 처리하기 위한 참조(ref)를 생성하고 업데이트된 product 상태 객체를 콘솔에 출력한다.
import React, { useRef, useState } from "react";
import { postAdd } from "../../api/productsApi";
const initState = {
pname: "",
pdesc: "",
price: 0,
files: [],
};
function AddComponent(props) {
const [product, setProduct] = useState(initState);
const uploadRef = useRef();
const handleChangeProduct = (e) => {
product[e.target.name] = e.target.value;
setProduct({ ...product });
};
const handleClickAdd = (e) => {
console.log(product);
const formData = new FormData();
const files = uploadRef.current.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);
console.log(formData);
postAdd(formData);
};
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Product Name</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="pname"
type={"text"}
value={product.pname}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Desc</div>
<textarea
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md resize-y"
name="pdesc"
rows="4"
onChange={handleChangeProduct}
value={product.pdesc}
>
{product.pdesc}
</textarea>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Price</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="price"
type={"number"}
value={product.price}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Files</div>
<input
ref={uploadRef}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
type={"file"}
multiple={true}
></input>
</div>
</div>
<div className="flex justify-end">
<div className="relative mb-4 flex p-4 flex-wrap items-stretch">
<button
type="button"
className="rounded p-4 w-36 bg-blue-500 text-xl text-white "
onClick={handleClickAdd}
>
ADD
</button>
</div>
</div>
</div>
);
}
export default AddComponent;
mall\src\components\products\AddComponent.js
임포트 할 때 productsApi인지 꼭 확인 !
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;
};
mall\src\api\productsApi.js
입력한 값이 페이로드와 서버에 insert로 뜨는지 확인
4. 결과 모달창 처리
import React from "react";
const FetchingModal = () => {
return (
<div
className={`fixed top-0 left-0 z-[1055] flex h-full w-full place-items-center justify-center bg-black bg-opacity-20`}
>
<div className=" bg-white rounded-3xl opacity-100 min-w-min h-1/4 flex justify-center items-center ">
<div className="text-4xl font-extrabold text-orange-400 m-20">
Loading.....
</div>
</div>
</div>
);
};
export default FetchingModal;
mall\src\components\common\FetchingModal.js
import React, { useRef, useState } from "react";
import { postAdd } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
const initState = {
pname: "",
pdesc: "",
price: 0,
files: [],
};
function AddComponent(props) {
const [product, setProduct] = useState(initState);
const uploadRef = useRef();
const [fetching, setFetching] = useState(false);
const handleChangeProduct = (e) => {
product[e.target.name] = e.target.value;
setProduct({ ...product });
};
const handleClickAdd = (e) => {
console.log(product);
const formData = new FormData();
const files = uploadRef.current.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);
console.log(formData);
setFetching(true);
postAdd(formData).then((data) => {
setFetching(false);
});
};
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Product Name</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="pname"
type={"text"}
value={product.pname}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Desc</div>
<textarea
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md resize-y"
name="pdesc"
rows="4"
onChange={handleChangeProduct}
value={product.pdesc}
>
{product.pdesc}
</textarea>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Price</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="price"
type={"number"}
value={product.price}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Files</div>
<input
ref={uploadRef}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
type={"file"}
multiple={true}
></input>
</div>
</div>
<div className="flex justify-end">
<div className="relative mb-4 flex p-4 flex-wrap items-stretch">
<button
type="button"
className="rounded p-4 w-36 bg-blue-500 text-xl text-white "
onClick={handleClickAdd}
>
ADD
</button>
</div>
</div>
{fetching ? <FetchingModal /> : <></>}
</div>
);
}
export default AddComponent;
mall\src\components\products\AddComponent.js
org/zerock/apiserver/service/ProductServiceTests.java
서버서 sleep 함수를 추가해서 자동으로 모달창이 닫히도록 한다.
이전과 같이 모달창을 만드는데 이 모달창은 버튼이 아니라 자동으로 닫힌다.
import React, { 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: [],
};
function AddComponent(props) {
const [product, setProduct] = useState(initState);
const uploadRef = useRef();
const [fetching, setFetching] = useState(false);
const [result, setResult] = useState(false);
const { moveToList } = useCustomMove();
const handleChangeProduct = (e) => {
product[e.target.name] = e.target.value;
setProduct({ ...product });
};
const handleClickAdd = (e) => {
console.log(product);
const formData = new FormData();
const files = uploadRef.current.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);
console.log(formData);
setFetching(true);
postAdd(formData).then((data) => {
setFetching(false);
setResult(data.result);
});
};
const closeModal = () => {
setResult(null);
moveToList({ page: 1 });
};
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Product Name</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="pname"
type={"text"}
value={product.pname}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Desc</div>
<textarea
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md resize-y"
name="pdesc"
rows="4"
onChange={handleChangeProduct}
value={product.pdesc}
>
{product.pdesc}
</textarea>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Price</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="price"
type={"number"}
value={product.price}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Files</div>
<input
ref={uploadRef}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
type={"file"}
multiple={true}
></input>
</div>
</div>
<div className="flex justify-end">
<div className="relative mb-4 flex p-4 flex-wrap items-stretch">
<button
type="button"
className="rounded p-4 w-36 bg-blue-500 text-xl text-white "
onClick={handleClickAdd}
>
ADD
</button>
</div>
</div>
{fetching ? <FetchingModal /> : <></>}
{result ? (
<ResultModal
callbackFn={closeModal}
title={"Product Add Result"}
content={`${result}번 상품 등록 필요`}
></ResultModal>
) : (
<></>
)}
</div>
);
}
export default AddComponent;
mall\src\components\products\AddComponent.js
기존에 있던 ResultModal을 사용하도록 추가
add 하면 결과 모달창이 뜸
그리고 close modal 버튼을 누르면 list 페이지로 다시 돌아가야 한다.
useCustomMove에 만들어둔 코드를 활용하여 해당 기능이 작동하도록 했다.
5. 상품 목록 컴포넌트 처리
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;
};
mall\src\api\productsApi.js
import React, { useEffect, useState } from "react";
import { getList } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import useCustomMove from "../../hooks/useCustomMove";
import { API_SERVER_HOST } from "../../api/todoApi";
import PageComponent from "../common/PageComponent";
const initState = {
dtoList: [],
pageNum: [],
pageRequestDTO: null,
prev: false,
next: false,
totalCount: 0,
prevPage: 0,
nextPage: 0,
totalPage: 0,
current: 0,
};
const host = API_SERVER_HOST;
function ListComponent() {
const { moveToList, moveToRead, page, size, refresh } = useCustomMove();
const [serverData, setServerData] = useState(initState);
const [fetching, setFetching] = useState(false);
useEffect(() => {
getList({ page, size }).then((data) => {
setFetching(false);
setServerData(data);
});
}, [page, size, refresh]);
return (
<div className="border-2 border-blue-100 mt-10 mr-2 ml-2">
{fetching ? <FetchingModal /> : <></>}
<div className="flex flex-wrap mx-auto p-6">
{serverData.dtoList.map((product) => (
<div
key={product.pno}
className="w-1/2 p-1 rounded shadow-md border-2"
onClick={() => moveToRead(product.pno)}
>
<div className="flex flex-col h-full">
<div className="font-extrabold text-2xl p-2 w-full ">
{product.pno}
</div>
<div className="text-1xl m-1 p-2 w-full flex flex-col">
<div className="w-full overflow-hidden ">
<img
alt="product"
className="m-auto rounded-md w-60"
src={`${host}/api/products/view/s_${product.uploadFileNames[0]}`}
/>
</div>
<div className="bottom-0 font-extrabold bg-white">
<div className="text-center p-1">이름: {product.pname}</div>
<div className="text-center p-1">가격: {product.price}</div>
</div>
</div>
</div>
</div>
))}
</div>
<PageComponent
serverData={serverData}
movePage={moveToList}
></PageComponent>
</div>
);
}
export default ListComponent;
mall\src\components\products\ListComponent.js
import React from "react";
import ListComponent from "../../components/products/ListComponent";
function ListPage(props) {
return (
<div className="p-4 w-full bg-white">
<div className="text-3xl font-extrabold">Products List Page</div>
<ListComponent />
</div>
);
}
export default ListPage;
mall\src\pages\products\ListPage.js
Products의 List에서 사진과 함께 조회할 수 있도록 했고, 이전에 만든 pageComponent를 재사용해서 페이지를 볼 수 있게 함
6. 상품 조회 컴포넌트 처리
지금은 항목에 들어가면 이러한 페이지가 뜬다. 이것을 해결하는 과정
import React from "react";
function ReadPage() {
return (
<div className="p-4 w-full bg-white">
<div className="text-3xl font-extrabold">Products Read Page</div>
</div>
);
}
export default ReadPage;
mall\src\pages\products\ReadPage.js
간단하게 ReadPage를 만들었다.
개발을 할 때 페이지를 만들면 다음은 무조건 라우터를 만들면 된다 !
import { lazy, Suspense } from "react";
import { Navigate } from "react-router-dom";
const Loading = <div>Loading....</div>;
const ProductList = lazy(() => import("../pages/products/ListPage"));
const ProductAdd = lazy(() => import("../pages/products/AddPage"));
const ProductRead = lazy(() => import("../pages/products/ReadPage"));
const productsRouter = () => {
return [
{
path: "list",
element: (
<Suspense fallback={Loading}>
<ProductList></ProductList>
</Suspense>
),
},
{
path: "",
element: <Navigate replace to={"/products/list"}></Navigate>,
},
{
path: "add",
element: (
<Suspense fallback={Loading}>
<ProductAdd></ProductAdd>
</Suspense>
),
},
{
path: "read/:pno",
element: (
<Suspense fallback={Loading}>
<ProductRead></ProductRead>
</Suspense>
),
},
];
};
export default productsRouter;
mall\src\router\productsRouter.js
read 페이지 관련 코드 추가
항목 하나를 클릭했을 때 read page가 뜨는지 확인
mall\src\api\productsApi.js
getOne 함수 - 특정 pno(상품 번호)로 데이터를 가져오는 비동기 함수
axios.get()을 사용하여 서버에서 데이터를 가져오고, 응답 데이터를 반환
import React, { useEffect, useState } from "react";
import { API_SERVER_HOST } from "../../api/todoApi";
import { getOne } from "../../api/productsApi";
const initState = {
pno: 0,
pname: "",
pdesc: "",
price: 0,
uploadFileNames: [],
};
const host = API_SERVER_HOST;
function ReadComponent({ pno }) {
const [product, setProduct] = useState(initState);
useEffect(() => {
getOne(pno).then((data) => {
console.log(data);
});
}, [pno]);
return <div>ReadComponent</div>;
}
export default ReadComponent;
mall\src\components\products\ReadComponent.js
import React from "react";
import { useParams } from "react-router-dom";
import ReadComponent from "../../components/products/ReadComponent";
function ReadPage() {
const { pno } = useParams();
return (
<div className="p-4 w-full bg-white">
<div className="text-3xl font-extrabold">Products Read Page</div>
<ReadComponent pno={pno} />
</div>
);
}
export default ReadPage;
mall\src\pages\products\ReadPage.js
데이터가 들어간 것을 확인할 수 있고 이것을 출력하면 된다.
import React, { useEffect, useState } from "react";
import { API_SERVER_HOST } from "../../api/todoApi";
import { getOne } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import useCustomMove from "../../hooks/useCustomMove";
const initState = {
pno: 0,
pname: "",
pdesc: "",
price: 0,
uploadFileNames: [],
};
const host = API_SERVER_HOST;
function ReadComponent({ pno }) {
const [product, setProduct] = useState(initState);
const [fetching, setFetching] = useState(false);
const { moveToList, moveToModify } = useCustomMove();
useEffect(() => {
setFetching(true);
getOne(pno).then((data) => {
console.log(data);
setProduct(data);
setFetching(false);
});
}, [pno]);
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
{fetching ? <FetchingModal /> : <></>}
<div className="flex justify-center mt-10">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">PNO</div>
<div className="w-4/5 p-6 rounded-r border border-solid shadow-md">
{product.pno}
</div>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">PNAME</div>
<div className="w-4/5 p-6 rounded-r border border-solid shadow-md">
{product.pname}
</div>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">PRICE</div>
<div className="w-4/5 p-6 rounded-r border border-solid shadow-md">
{product.price}
</div>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">PDESC</div>
<div className="w-4/5 p-6 rounded-r border border-solid shadow-md">
{product.pdesc}
</div>
</div>
</div>
<div className="w-full justify-center flex flex-col m-auto items-center">
{product.uploadFileNames.map((imgFile, i) => (
<img
alt="product"
key={i}
className="p-4 w-1/2"
src={`${host}/api/products/view/${imgFile}`}
/>
))}
</div>
<div className="flex justify-end p-4">
<button
type="button"
className="inline-block rounded p-4 m-2 text-xl w-32 text-white bg-red-500"
onClick={() => moveToModify(pno)}
>
Modify
</button>
<button
type="button"
className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
onClick={moveToList}
>
List
</button>
</div>
</div>
);
}
export default ReadComponent;
mall\src\components\products\ReadComponent.js
상품 조회 코드까지 작성하면 데이터와 이미지가 전부 출력됨
7. 상품 수정/삭제 컴포넌트 처리(1)
import React from "react";
import { useParams } from "react-router-dom";
import ModifyComponent from "../../components/products/ModifyComponent";
function ModifyPage() {
const { pno } = useParams();
return (
<div className="p-4 w-full bg-white">
<div className="text-3xl font-extrabold">Products Modify Page</div>
<ModifyComponent pno={pno}></ModifyComponent>
</div>
);
}
export default ModifyPage;
mall\src\pages\products\ModifyPage.js
mall\src\router\productsRouter.js
import React from "react";
function ModifyComponent({ pno }) {
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">ModifyComponent</div>
);
}
export default ModifyComponent;
mall\src\components\products\ModifyComponent.js
mall\src\api\productsApi.js
삭제와 수정 API
ModifyComponent가 출력되는지 확인
import React, { useEffect, useRef, useState } from "react";
import { getOne } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import { API_SERVER_HOST } from "../../api/todoApi";
const initState = {
pno: 0,
pname: "",
pdesc: "",
price: 0,
delFlag: false,
uploadFileNames: [],
};
const host = API_SERVER_HOST;
function ModifyComponent({ pno }) {
const [product, setProduct] = useState(initState);
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 });
};
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
{fetching ? <FetchingModal /> : <></>}
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Product Name</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="pname"
type={"text"}
value={product.pname}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Desc</div>
<textarea
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md resize-y"
name="pdesc"
rows="4"
onChange={handleChangeProduct}
value={product.pdesc}
>
{product.pdesc}
</textarea>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Price</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="price"
type={"number"}
value={product.price}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">DELETE</div>
<select
name="delFlag"
value={product.delFlag}
onChange={handleChangeProduct}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
>
<option value={false}>사용</option>
<option value={true}>삭제</option>
</select>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Files</div>
<input
ref={uploadRef}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
type={"file"}
multiple={true}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Images</div>
<div className="w-4/5 justify-center flex flex-wrap items-start">
{product.uploadFileNames.map((imgFile, i) => (
<div className="flex justify-center flex-col w-1/3" key={i}>
<button
className="bg-blue-500 text-3xl text-white"
onClick={() => deleteOldImages(imgFile)}
>
DELETE
</button>
<img alt="img" src={`${host}/api/products/view/s_${imgFile}`} />
</div>
))}
</div>
</div>
</div>
</div>
);
}
export default ModifyComponent;
mall\src\components\products\ModifyComponent.js
DELETE를 누르면 이미지가 사라지지만 실제로 삭제된 것은 아니다.
8. 상품 수정/삭제 컴포넌트 처리(2)
import React, { useEffect, useRef, useState } from "react";
import { getOne, putOne } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import { API_SERVER_HOST } from "../../api/todoApi";
const initState = {
pno: 0,
pname: "",
pdesc: "",
price: 0,
delFlag: false,
uploadFileNames: [],
};
const host = API_SERVER_HOST;
function ModifyComponent({ pno }) {
const [product, setProduct] = useState(initState);
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]);
}
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]);
}
putOne(pno, formData);
};
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
{fetching ? <FetchingModal /> : <></>}
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Product Name</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="pname"
type={"text"}
value={product.pname}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Desc</div>
<textarea
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md resize-y"
name="pdesc"
rows="4"
onChange={handleChangeProduct}
value={product.pdesc}
>
{product.pdesc}
</textarea>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Price</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="price"
type={"number"}
value={product.price}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">DELETE</div>
<select
name="delFlag"
value={product.delFlag}
onChange={handleChangeProduct}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
>
<option value={false}>사용</option>
<option value={true}>삭제</option>
</select>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Files</div>
<input
ref={uploadRef}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
type={"file"}
multiple={true}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Images</div>
<div className="w-4/5 justify-center flex flex-wrap items-start">
{product.uploadFileNames.map((imgFile, i) => (
<div className="flex justify-center flex-col w-1/3" key={i}>
<img alt="img" src={`${host}/api/products/view/s_${imgFile}`} />
</div>
))}
</div>
</div>
</div>
<div className="flex justify-end p-4">
<button
type="button"
className="rounded p-4 m-2 text-xl w-32 text-white bg-red-500"
onClick={handleClickDelete}
>
Delete
</button>
<button
type="button"
className="inline-block rounded p-4 m-2 text-xl w-32 text-white bg-orange-500"
onClick={handleClickModify}
>
Modify
</button>
<button
type="button"
className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
>
List
</button>
</div>
</div>
);
}
export default ModifyComponent;
mall\src\components\products\ModifyComponent.js
Modify 버튼을 누르고 페이로드를 확인하면 2개의 이미지가 있는 것을 볼 수 있고
다시 조회를 하면 이미지가 추가되어 3개가 된 것을 확인할 수 있다.
그리고 하나를 삭제하면 2개로 줄고 이 상태가 유지된다.
import React, { useEffect, useRef, useState } from "react";
import { getOne, putOne } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import { API_SERVER_HOST } from "../../api/todoApi";
import useCustumMove from "../../hooks/useCustomMove";
import ResultModal from "../common/ResultModal";
const initState = {
pno: 0,
pname: "",
pdesc: "",
price: 0,
delFlag: false,
uploadFileNames: [],
};
const host = API_SERVER_HOST;
function ModifyComponent({ pno }) {
const [product, setProduct] = useState(initState);
const [fetching, setFetching] = useState(false);
const [result, setResult] = useState(false);
const { moveToList, moveToRead } = useCustumMove();
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]);
}
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]);
}
setFetching(true);
putOne(pno, formData).then((data) => {
setResult("Modified");
setFetching(false);
});
};
const closeModal = () => {
if (result === "Modified") {
moveToRead(pno);
}
setResult(null);
};
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
{fetching ? <FetchingModal /> : <></>}
{result ? (
<ResultModal
title={`${result}`}
content={"처리되었습니다."}
callbackFn={closeModal}
></ResultModal>
) : (
<></>
)}
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Product Name</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="pname"
type={"text"}
value={product.pname}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Desc</div>
<textarea
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md resize-y"
name="pdesc"
rows="4"
onChange={handleChangeProduct}
value={product.pdesc}
>
{product.pdesc}
</textarea>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Price</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="price"
type={"number"}
value={product.price}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">DELETE</div>
<select
name="delFlag"
value={product.delFlag}
onChange={handleChangeProduct}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
>
<option value={false}>사용</option>
<option value={true}>삭제</option>
</select>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Files</div>
<input
ref={uploadRef}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
type={"file"}
multiple={true}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Images</div>
<div className="w-4/5 justify-center flex flex-wrap items-start">
{product.uploadFileNames.map((imgFile, i) => (
<div className="flex justify-center flex-col w-1/3" key={i}>
<button
className="bg-blue-500 text-3xl text-white"
onClick={() => deleteOldImages(imgFile)}
>
DELETE
</button>
<img alt="img" src={`${host}/api/products/view/s_${imgFile}`} />
</div>
))}
</div>
</div>
</div>
<div className="flex justify-end p-4">
<button
type="button"
className="inline-block rounded p-4 m-2 text-xl w-32 text-white bg-orange-500"
onClick={handleClickModify}
>
Modify
</button>
<button
type="button"
className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
>
List
</button>
</div>
</div>
);
}
export default ModifyComponent;
Close Modal 버튼을 누르면 수정된 후에 다시 항목 페이지로 돌아간다.
import React, { useEffect, useRef, useState } from "react";
import { deleteOne, getOne, putOne } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import { API_SERVER_HOST } from "../../api/todoApi";
import useCustumMove from "../../hooks/useCustomMove";
import ResultModal from "../common/ResultModal";
const initState = {
pno: 0,
pname: "",
pdesc: "",
price: 0,
delFlag: false,
uploadFileNames: [],
};
const host = API_SERVER_HOST;
function ModifyComponent({ pno }) {
const [product, setProduct] = useState(initState);
const [fetching, setFetching] = useState(false);
const [result, setResult] = useState(false);
const { moveToList, moveToRead } = useCustumMove();
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]);
}
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]);
}
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 (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
{fetching ? <FetchingModal /> : <></>}
{result ? (
<ResultModal
title={`${result}`}
content={"처리되었습니다."}
callbackFn={closeModal}
></ResultModal>
) : (
<></>
)}
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Product Name</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="pname"
type={"text"}
value={product.pname}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Desc</div>
<textarea
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md resize-y"
name="pdesc"
rows="4"
onChange={handleChangeProduct}
value={product.pdesc}
>
{product.pdesc}
</textarea>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Price</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="price"
type={"number"}
value={product.price}
onChange={handleChangeProduct}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">DELETE</div>
<select
name="delFlag"
value={product.delFlag}
onChange={handleChangeProduct}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
>
<option value={false}>사용</option>
<option value={true}>삭제</option>
</select>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Files</div>
<input
ref={uploadRef}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
type={"file"}
multiple={true}
></input>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">Images</div>
<div className="w-4/5 justify-center flex flex-wrap items-start">
{product.uploadFileNames.map((imgFile, i) => (
<div className="flex justify-center flex-col w-1/3" key={i}>
<button
className="bg-blue-500 text-3xl text-white"
onClick={() => deleteOldImages(imgFile)}
>
DELETE
</button>
<img alt="img" src={`${host}/api/products/view/s_${imgFile}`} />
</div>
))}
</div>
</div>
</div>
<div className="flex justify-end p-4">
<button
type="button"
className="inline-block rounded p-4 m-2 text-xl w-32 text-white bg-red-500"
onClick={handleClickDelete}
>
Delete
</button>
<button
type="button"
className="inline-block rounded p-4 m-2 text-xl w-32 text-white bg-orange-500"
onClick={handleClickModify}
>
Modify
</button>
<button
type="button"
className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
onClick={() => {
moveToList();
}}
>
List
</button>
</div>
</div>
);
}
export default ModifyComponent;
Delete 처리
List 버튼을 눌렀을 때 list 페이지로 가도록 이벤트 처리
'Web' 카테고리의 다른 글
8. 리덕스 툴킷 (0) | 2024.10.30 |
---|---|
7. 시큐리티와 API 서버 (0) | 2024.10.27 |
5. 상품 API 서버 구성하기 - 2 (0) | 2024.10.14 |
5. 상품 API 서버 구성하기 - 1 (0) | 2024.10.08 |
4. 리액트와 API 서버 통신 (0) | 2024.10.06 |