게시물 등록하기 페이지 구현React+REST API 게시판 구현/FE - React2021. 10. 11. 21:18
Table of Contents
What to do?
- 게시물의 제목과 내용을 입력하여 DB에 반영하는 화면을 구현한다.
- 보안을 위해 HTTP Header에 jwt-token의 정보를 넘겨주는 구현해둔 axios intercepter에 api 요청을 한다.
- 게시물 등록하기 화면은 로그인 정보(jwt-token)가 없으면 들어갈 수 없는 화면이므로 PrivateRoute 컴포넌트로 라우팅 처리해서 로그인하는 과정을 거치게 한다.
- 게시물의 사진, 제목, 내용 중 하나라도 입력을 안하면 제출할 수 없도록 막고 DB에 성공적으로 데이터가 반영이 되면 url을 /board-list로 이동한다.
- 게시물을 등록할 때 사용할 이미지 업로더와 title, content의 input은 게시판 수정할 때 재사용할 것이므로 컴포넌트화 시킨다!
- 등록에 성공하면 DB에 제목, 내용, board의 id에 해당하는 사진이 잘 올라간 모습이다!
이미지 업로더 컴포넌트 구현하기
- components 폴더 아래 ImageUploader.js 파일과 imageUploader.scss 파일을 생성하자
components/ImageUploader.js
- ImageUploader는 props로 미리보기 url과 setImage라는 state를 변경하는 함수를 받는데 setImage는 file input에서 올라온 파일 객체와 미리보기 url을 설정해주는 함수이다
- 게시판 등록 컴포넌트에서 이미지 올리기전에 미리볼 수 있는 디폴트 이미지의 주소 preview_URL과 image라는 상태를 변경시키는 setImage 함수를 넘겨준다.
- 즉, ImageUploader 컴포넌트의 fille input에서 파일을 올리고 setImage 함수를 사용하면 게시판 등록 컴포넌트의 image 상태에 실제 이미지 파일 객체와 미리보기 주소가 들어간다.
- 게시판 수정하기 페이지에서도 사용할 것이므로 따로 컴포넌트화 시켰다
import "./imageUploader.scss";
import { Button } from "@mui/material";
const ImageUploader = ({ preview_URL, setImage }) => {
let inputRef;
const saveImage = (e) => {
e.preventDefault();
const fileReader = new FileReader();
if (e.target.files[0]) {
fileReader.readAsDataURL(e.target.files[0]);
}
fileReader.onload = () => {
setImage({
image_file: e.target.files[0],
preview_URL: fileReader.result,
});
};
};
return (
<div className="uploader-wrapper">
<input
type="file"
accept="image/*"
onChange={saveImage}
ref={(refParam) => (inputRef = refParam)}
style={{ display: "none" }}
/>
<div className="img-wrapper">
<img src={preview_URL} />
</div>
<div className="upload-button">
<Button
variant="outlined"
color="primary"
onClick={() => inputRef.click()}
>
😎사진 고르기😎
</Button>
</div>
</div>
);
};
export default ImageUploader;
components/imageUploader.scss
.uploader-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 0 15px;
.img-wrapper {
img {
width: 270px;
height: 270px;
object-fit: cover;
}
}
.upload-button {
button {
margin: 10px 5px;
font-size: 1.1rem;
}
}
}
제목, 내용 입력 컴포넌트 구현
- components 디렉토리 아래 TextArea.js 파일과 textArea.scss 파일을 만들자.
components/TextArea.js
- props로 제목 상태와 제목 상태를 변경하는 함수, 내용상태와 내용 상태를 변경하는 함수를 받아서 제목을 입력하는 input과 내용을 입력하는 input에 onChange 함수를 넣어서 제목과 내용을 입력할 때마다 게시판 등록하기 컴포넌트의 제목과 입력 상태를 변하게 한다
import "./textArea.scss";
const TextArea = ({ setTitle, setContent, title, content }) => {
return (
<div className="textArea-wrapper">
<input
onChange={(e) => {
setTitle(e.target.value);
}}
className="title"
placeholder="제목을 입력하세요"
value={title}
/>
<textarea
onChange={(e) => {
setContent(e.target.value);
}}
className="text"
placeholder="내용을 입력하세요"
value={content}
/>
</div>
);
};
export default TextArea;
components/textArea.scss
.textArea-wrapper {
margin: 0 10px;
display: flex;
flex-direction: column;
.title {
margin-bottom: 0.7rem;
}
.text {
width: 270px;
height: 270px;
}
input, textarea {
&::-webkit-scrollbar {
display: none;
}
resize: none;
font-size: 18px;
font-weight: 500;
font-family: 'Noto Sans KR', sans-serif;
border: 1px solid whitesmoke;
border-radius: 5px;
transition: border 1s;
padding: 5px;
box-sizing: border-box;
&:focus {
outline: none;
border: 3px solid skyblue;
}
}
}
게시물 등록 페이지 구현
- pages 디렉토리 아래 add-board 디렉토리를 만들고 AddBoard.js 파일과 addBoard.scss 파일을 만들자.
- add-board라는 url에 렌더링 될 화면이고 위에서 만든 ImageUploader 컴포넌트와 TextArea 컴포넌트를 사용한다.
- 그리고 아직 사진을 선택하지 않을 경우를 대비해서 public 디렉토리 아래 image 파일을 넣고 미리 보여줄 디폴트 이미지를 하나 넣어주자
pages/add-board/Addboard.js
- title, content, image 세가지 상태를 가지며 각각 제목, 내용, 사진 파일에 대한 정보를 담고있다. 맨 처음에는 등록된 사진이 없어서 preview_URL에 디폴트 이미지의 경로를 넣어줬다.
- 위에서 만든 이미지 업로더와 제목, 내용을 입력하는 컴포넌트에 props로 제목, 내용, 사진의 상태를 변화하는 함수를 넣어줬고 제목, 내용, 사진 중 하나라도 입력되지 않으면 게시물이 등록되지 않는다!
- 인증된 사용자만 이용할 수 있는 기능이므로 미리 정의한 api라는 axios interceptor로 user의 id를 토큰에서 얻은 후 같이 서버에 요청을 보낸다.
import {useSelector} from "react-redux";
import {useNavigate} from "react-router-dom";
import {useCallback, useState} from "react";
import ImageUploader from "../../components/ImageUploader";
import api from "../../utils/api";
import {jwtUtils} from "../../utils/jwtUtils";
import TextArea from "../../components/TextArea";
import {Button} from "@mui/material";
import "./addBoard.scss";
import {toast} from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
const AddBoard = () => {
const token = useSelector(state => state.Auth.token);
const navigate = useNavigate();
// 게시판 제목, 내용, 사진
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [image, setImage] = useState({
image_file: "",
preview_URL: "image/default_image.png",
});
const canSubmit = useCallback(() => {
return image.image_file !== "" && content !== "" && title !== "";
}, [image, title, content]);
const handleSubmit = useCallback(async () => {
try{
const formData = new FormData();
formData.append("title", title);
formData.append("content", content);
formData.append("file", image.image_file);
formData.append("user_id", jwtUtils.getId(token));
await api.post("/api/board", formData);
window.alert("😎등록이 완료되었습니다😎");
navigate("/board-list");
} catch (e) {
// 서버에서 받은 에러 메시지 출력
toast.error("오류발생! 이모지를 사용하면 오류가 발생할 수 있습니다" + "😭", {
position: "top-center",
});
}
}, [canSubmit]);
return (
<div className="addBoard-wrapper">
<div className="addBoard-header">
게시물 등록하기 🖊️
</div>
<div className="submitButton">
{canSubmit() ? (
<Button
onClick={handleSubmit}
className="success-button"
variant="outlined"
>
등록하기😃
</Button>
) : (
<Button
className="disable-button"
variant="outlined"
size="large"
>
사진과 내용을 모두 입력하세요😭
</Button>
)}
</div>
<div className="addBoard-body">
<ImageUploader setImage={setImage} preview_URL={image.preview_URL}/>
<TextArea setTitle={setTitle} setContent={setContent} title={title} content={content}/>
</div>
</div>
);
}
export default AddBoard;
pages/add-board/addBoard.scss
- 이미지가 업로드 컴포넌트에서 이미지가 불려올 때 어느정도 딜레이가 존재하므로 애니메이션을 줘서 서서히 나타나게 구현하자!
@keyframes smoothAppear {
from {
opacity: 0;
transform: translateY(-5%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.addBoard-wrapper {
opacity: 0;
animation: smoothAppear 1s forwards;
animation-delay: 0.5s;
font-family: 'Noto Sans KR', sans-serif;
.addBoard-header{
text-align: center;
font-size: 32px;
font-weight: bold;
margin: 20px 0;
}
.submitButton{
padding: 20px 0;
display: flex;
align-items: center;
justify-content: center;
button{
}
.disable-button{
font-size: 1.1rem;
cursor: not-allowed;
}
.success-button{
font-size: 1.1rem;
}
}
.addBoard-body {
display: flex;
margin: 20px 0;
justify-content: center;
flex-wrap: wrap;
}
}
라우팅 적용하기
App.js
- 로그인한 사용자만 이용할 수 있으므로 PrivateRoute로 처리한다
import React from "react";
import {Routes, Route} from "react-router-dom";
import Header from "./components/Header";
import Home from "./pages/home/Home";
import SignUp from "./pages/sign-up/SignUp";
import Login from "./pages/login/Login";
import PrivateRoute from "./routes/PrivateRoute";
import AddBoard from "./pages/add-board/AddBoard";
const App = () => {
return (
<React.Fragment>
<Header/>
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/sign-up" element={<SignUp/>}/>
<Route path="/login" element={<Login/>}/>
<Route
path="/add-board"
element={
<PrivateRoute path="/add-board" component={AddBoard}/>
}
/>
</Routes>
</React.Fragment>
)
}
export default App;
'React+REST API 게시판 구현 > FE - React' 카테고리의 다른 글
게시물 상세보기 페이지 구현 (0) | 2021.10.18 |
---|---|
게시판/내 게시물 페이지 구현 - 페이지네이션 (0) | 2021.10.13 |
axios interceptor 구현하기 (0) | 2021.10.04 |
인증 처리하기+PrivateRoute+로그아웃 (0) | 2021.10.04 |
로그인 구현 (redux-persist) (0) | 2021.10.02 |
@덕구공 :: Duck9s'
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!