일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 오라클
- oracle
- 자동차수리시스템
- 어윈 사용법
- NestedFor
- 집합_SET
- GRANT VIEW
- 추상메서드
- cursor문
- 컬렉션프레임워크
- exception
- 생성자오버로드
- 제네릭
- 자바
- 정수형타입
- EnhancedFor
- 예외미루기
- 컬렉션 타입
- 환경설정
- Java
- 예외처리
- 한국건설관리시스템
- 다형성
- abstract
- 메소드오버로딩
- 대덕인재개발원
- 사용자예외클래스생성
- 참조형변수
- 인터페이스
- 객체 비교
- Today
- Total
거니의 velog
(26) 리액트와 상품 API 서버 연동 5 본문
5. 수정/삭제 페이지와 컴포넌트 처리
* 수정/삭제를 위한 페이지는 pages/products/ModifyPage.js 파일로 작성하고, router/productsRouter.js 파일을 수정해서 라우팅 설정을 추가한다.
import React from "react";
const ModifyPage = () => {
return (
<div className="p-4 w-full bg-white">
<div className="text-3xl font-extrabold">Products Modify Page</div>
</div>
);
};
export default ModifyPage;
* router/productsRouter.js 내 ModifyPage 설정을 추가한다.
import React, { Suspense, lazy } from "react";
import { Navigate } from "react-router-dom";
const productsRouter = () => {
const Loading = <div>Loading....</div>;
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"));
return [
{
path: "list",
element: (
<Suspense fallback={Loading}>
<ProductsList />
</Suspense>
),
},
{
path: "",
element: <Navigate replace to="/products/list" />,
},
{
path: "add",
element: (
<Suspense fallback={Loading}>
<ProductsAdd />
</Suspense>
),
},
{
path: "read/:pno",
element: (
<Suspense fallback={Loading}>
<ProductRead />
</Suspense>
),
},
{
path: "modify/:pno",
element: (
<Suspense fallback={Loading}>
<ProductModify />
</Suspense>
),
},
];
};
export default productsRouter;
* 상품의 조회 화면에서 수정/삭제 화면으로 정상적으로 이동되는지 확인한다.
(1) ModifyComponent 처리
* 수정 작업은 등록과 마찬가지로 첨부파일이 존재하기 때문에 multipart/form-data 헤더를 설정해서 전송 처리해야 하고 삭제 작업은 해당 상품의 번호만을 전달해서 처리한다.
* 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;
};
* components/products 폴더에는 ModifyComponent를 추가한다.
import React from "react";
const ModifyComponent = ({ pno }) => {
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
Product Modify Component
</div>
);
};
export default ModifyComponent;
* ModifyComponent는 ModifyPage에서 import 하도록 추가한다. 이때 현재 상품번호를 전달하도록 구성한다.
import React from "react";
import { useParams } from "react-router-dom";
import ModifyComponent from "../../components/products/ModifyComponent";
const 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} />
</div>
);
};
export default ModifyPage;
* 프로젝트를 실행하고 브라우저에서 목록 -> 조회 -> 수정의 단계로 결과를 확인한다. 현재 ModifyComponent의 디자인이 없기 때문에 화면은 아래와 같이 단순 텍스트만 출력된다.
[데이터의 출력]
* ModifyComponent는 전달되는 pno를 속성으로 처리하고 서버와 통신 처리 후 결과를 출력한다. FetchingModal을 이용해서 서버와의 통신 상태를 알 수 있게 처리하고 이미지들을 출력한다.
import React, { useEffect, useState } from "react";
import { getOne } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
const initState = {
pno: 0,
pname: "",
pdesc: "",
price: 0,
delFlag: false,
uploadFileNames: [],
};
const ModifyComponent = ({ pno }) => {
const [product, setProduct] = useState(initState);
const [fetching, setFetching] = useState(false);
useEffect(() => {
setFetching(true);
getOne(pno).then((data) => {
setProduct(data);
setFetching(false);
});
}, [pno]);
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
Product Modify Component
{fetching ? <FetchingModal /> : <></>}
</div>
);
};
export default ModifyComponent;
* 브라우저에서는 FetchingModal 창을 통해서 서버와 통신이 이루어졌음을 확인할 수 있다(아래 화면은 API 서버의 응답 시간을 조정해서 FetchingModal 이 보일 수 있는 시간을 준 상태이다).
* 서버에서 가져온 상품정보를 화면에 출력하고 변경이 가능한 속성들을 <input>으로 작성하고 onChange()를 이용해서 변경 가능하도록 구성한다.
import React, { useEffect, useState, useRef } 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;
const 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) => {};
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;
* 브라우저에서는 해당 상품의 이미지들이 출력되고 입력창은 수정이 가능한 상태가 된다.
[기존 이미지 삭제]
* ModifyComponent에서 기존 이미지의 삭제는 이미지 상단에 있는 DELETE 버튼을 클릭했을 때 상품 데이터가 가지고 있는 uploadFileNames 배열에서 해당 이미지를 삭제한다. 배열에서는 filter()를 사용해서 해당 이미지가 아닌 이미지들만 유지하도록 한다.
const deleteOldImages = (imageName) => {
const resultFileNames = product.uploadFileNames.filter(
(fileName) => fileName !== imageName
);
product.uploadFileNames = resultFileNames;
setProduct({ ...product });
};
* 상품의 이미지에 있는 버튼을 클릭해서 해당 이미지를 배열에서 제거할 수 있도록 이벤트 처리를 추가한다.
<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>
* 브라우저 화면에서는 기존 상품 이미지의 삭제를 확인할 수 있다. 이미지의 삭제는 화면 상에서만 반영되기 때문에 다시 조회해 보면 원래의 모든 이미지를 볼 수 있다.
[새로운 이미지 파일의 추가와 수정]
* 새로운 이미지 파일을 추가하는 부분은 기존의 신규 상품등록과 동일하므로 화면에 수정 버튼을 작성하고 이를 클릭할 때 파일 데이터를 추가하도록 구성한다.
* ModifyComponent 내에 이벤트 처리를 위한 handleClickModify() 함수를 추가하고 가장 아래쪽에 Modify 버튼을 추가해서 이벤트 처리를 완성한다.
import { getOne, putOne } from "../../api/productsApi";
(...)
const ModifyComponent = ({ pno }) => {
(...)
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]);
}
putOne(pno, formData);
};
(...)
}
* 화면에서는 마지막 부분에 버튼들을 추가하고 버튼 중에서 Modify에 대해서 이벤트 처리를 추가한다.
(...)
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
(...)
<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"
>
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;
* 화면 상에 새로운 이미지를 추가하고 Modify 버튼을 클릭해서 서버의 동작 여부를 먼저 확인한다(모달창의 처리가 없으므로 아직 화면에 변화는 없다). 기존의 이미지를 삭제하지 않았다면 브라우저에서 '새로고침'을 통해서 추가된 이미지를 확인할 수 있다.
[수정 작업 후 모달창]
* Axios를 이용한 상품의 수정 처리가 완료되면 서버에서는 아래 화면과 같은 JSON 결과를 전송한다.
* ModifyComponent에서는 모달창을 이용해서 결과를 보여주도록 수정한다. 우선 모달창에서 필요한 상태와 모달창이 사라진 후 동작해야 하는 함수를 미리 정의한다. useCustomMove를 이용해서 수정은 상품의 조회 화면으로 이동하고, 상품의 삭제 후에는 목록 화면으로 이동하도록 구성한다.
import React, { useEffect, useState, useRef } from "react";
import { getOne, putOne } 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 = {
(...)
};
const host = API_SERVER_HOST;
const ModifyComponent = ({ pno }) => {
const [product, setProduct] = useState(initState);
const [fetching, setFetching] = useState(false);
const uploadRef = useRef();
//결과 모달
const [result, setResult] = useState(null);
//이동용 함수
const { moveToRead, moveToList } = useCustomMove();
(...)
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 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}
/>
) : (
<></>
)}
(...)
</div>
);
};
export default ModifyComponent;
* 상품을 수정하면 ResultModal을 보여주고 조회 화면으로 이동하는 것을 확인할 수 있다.
(2) 삭제 버튼의 동작 처리
* 상품의 삭제는 화면 아래쪽의 Delete 버튼을 클릭하면 동작하는 함수를 작성하고 삭제 후 모달창을 띄우도록 작성한다.
import React, { useEffect, useState, useRef } from "react";
import { getOne, putOne, deleteOne } from "../../api/productsApi";
(...)
const initState = {
(...)
};
const host = API_SERVER_HOST;
const ModifyComponent = ({ pno }) => {
(...)
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);
};
(...)
};
export default ModifyComponent;
* 화면 아래쪽의 버튼에는 이벤트를 추가하고 삭제 처리 후 목록 화면으로 이동하는지 확인한다.
<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>
(...)
</div>
[목록 화면 이동]
* 목록 화면의 이동은 useCustomMove의 moveToList를 이용해서 버튼에 이벤트를 추가한다.
<div className="flex justify-end p-4">
(...)
<button
type="button"
className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
onClick={moveToList}
>
List
</button>
</div>
* 화면 상에서는 쿼리스트링이 유지된 상태로 목록 화면으로 이동하게 된다.
'SpringBoot_React 풀스택 프로젝트' 카테고리의 다른 글
(28) 스프링 시큐리티와 API 서버 2 (0) | 2024.03.05 |
---|---|
(27) 스프링 시큐리티와 API 서버 1 (0) | 2024.03.05 |
(25) 리액트와 상품 API 서버 연동 4 (0) | 2024.03.05 |
(24) 리액트와 상품 API 서버 연동 3 (0) | 2024.03.05 |
(23) 리액트와 상품 API 서버 연동 2 (1) | 2024.03.05 |