본문 바로가기

Java/Spring

[Spring] 간단한 SNS 만들기 #4

What to do?

React로 Front-end 프로젝트 생성하기

ⓐ 네비게이션바  ⓑ 회원가입  ⓒ 로그인  ⓓ포스팅 List  ⓔ 포스팅 작성

 

회원가입
로그인 및 포스팅 작성

 


세팅

 

프로젝트 생성

  • front-end라는 이름의 react 프로젝트 생성
npx create-react-app front-end

라이브러리 설치

  • recoil : 상태관리
  • react-router-dom : 라우팅
  • material ui : 디자인
npm install recoil
npm install react-router-dom		# 최신버전(v6)
npm install @mui/material @emotion/react @emotion/styled
npm install @mui/icons-material

index.js

 

App태그를 RecoilRoot 태그, BrowserRouter 태그로 감싸줌

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <RecoilRoot>
    <React.StrictMode>
      <BrowserRouter>
          <App />
      </BrowserRouter>
    </React.StrictMode>
  </RecoilRoot>
);

reportWebVitals();

index.js


전역 State 정의

 

recoil/index.js

export * from "./user";

 

recoil/user.js

import { atom } from "recoil";

export const userState = atom({
    key : "userState",
    default : {
        nickname:null,
        token:null
    }
})


App.js

 

  • 라우팅 경로 지정
const App = () => {
  return (
  <div className="App">
    <Nav/>
    <Routes>
      <Route path="/register" element={<Register/>}></Route>
      <Route path="/login" element={<Login/>}></Route>
      <Route path="/post" element={<Post/>}></Route>
      <Route path="/post/:id" element={<DetailPost/>}></Route>
      <Route path="/post/write" element={<WritePost/>}></Route>
    </Routes>
  </div>
  );
}

export default App;

Material UI

 

css파일 만지면서 한땀한땀 디자인을 넣기는 너무 빡세다.

그냥 material ui에서 코드 북붙하고 수정해서 만들었다.

https://mui.com/material-ui/react-app-bar/#basic-app-bar


네비게이션바

 

import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Menu from '@mui/material/Menu';
import MenuIcon from '@mui/icons-material/Menu';
import Container from '@mui/material/Container';
import Button from '@mui/material/Button';
import MenuItem from '@mui/material/MenuItem';
import AdbIcon from '@mui/icons-material/Adb';
import { Link } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import { userState } from '../../recoil/user';
import { useEffect, useState } from 'react';
import axios from 'axios';

// Custom Settings
const appName = "Karma"
const pagesNotLogined = [
  {label:"login", link:"/login"},
  {label:"register", link:"/register"},
]

const pagesLogined = [
  {label:"post", link:"/post"}
]

// Component to export
const Nav = () => {
  const [user, setUser] = useRecoilState(userState);
  const [pages, setPages] = useState([{}]);
  const [anchorElNav, setAnchorElNav] = useState(null);

  useEffect(()=>{
    // localStorage에서 token값 꺼내기
    const token = localStorage.getItem("token");
    // token이 없으면 초기화
    if (!token){
      setPages(pagesNotLogined);
      setUser({nickname:null, token: null});
      localStorage.removeItem("token");
      return;
    }    
    // token이 유효한지 요청
    const endPoint = "/api/v1/user/nickname";
    axios.get(endPoint, {
      headers:{
        Authorization : localStorage.getItem("token")
      }
    }).then((res)=>{
      return res.data.result
    }).then((nickname)=>{
      setPages(pagesLogined);
      setUser({token, nickname});
    }).catch((err)=>{
      console.log(err);
    })
  }, [])

  const handleOpenNavMenu = (event) => {
    setAnchorElNav(event.currentTarget);
  };
  const handleCloseNavMenu = () => {
    setAnchorElNav(null);
  };

  return (
    <AppBar position="static">
      <Container maxWidth="xl">
        <Toolbar disableGutters>
          <AdbIcon sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} />
          <Link to="/">
            <Typography
              variant="h6"
              noWrap
              sx={{
                mr: 2,
                display: { xs: 'none', md: 'flex' },
                fontFamily: 'monospace',
                fontWeight: 700,
                letterSpacing: '.3rem',
                color: 'white',
                textDecoration: 'none',
              }}>
              {appName}
            </Typography>
          </Link>

          <Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }}>
            <IconButton
              size="large"
              aria-label="account of current user"
              aria-controls="menu-appbar"
              aria-haspopup="true"
              onClick={handleOpenNavMenu}
              color="inherit"
            >
              <MenuIcon />
            </IconButton>
            <Menu
              id="menu-appbar"
              anchorEl={anchorElNav}
              anchorOrigin={{
                vertical: 'bottom',
                horizontal: 'left',
              }}
              keepMounted
              transformOrigin={{
                vertical: 'top',
                horizontal: 'left',
              }}
              open={Boolean(anchorElNav)}
              onClose={handleCloseNavMenu}
              sx={{
                display: { xs: 'block', md: 'none' },
              }}
            >
              {pages.map((page, idx) => (
                <Link to={page.link} key={idx}>
                  <MenuItem onClick={handleCloseNavMenu}>
                    <Typography textAlign="center">{page.label}</Typography>
                  </MenuItem>
                </Link>
              ))}
            </Menu>
          </Box>
          <AdbIcon sx={{ display: { xs: 'flex', md: 'none' }, mr: 1 }} />
          <Typography
            variant="h5"
            noWrap
            component="a"
            href=""
            sx={{
              mr: 2,
              display: { xs: 'flex', md: 'none' },
              flexGrow: 1,
              fontFamily: 'monospace',
              fontWeight: 700,
              letterSpacing: '.3rem',
              color: 'inherit',
              textDecoration: 'none',
            }}
          >
            {appName}
          </Typography>

          <Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
            {pages.map((page, idx) => (
              <Link to={page.link} sx={{color: 'white'}}>
                <Button
                  key={idx}
                  onClick={handleCloseNavMenu}
                  sx={{ my: 2, color: 'white', display: 'block' }}>
                  {page.label}
                </Button>
              </Link>
            ))}
          </Box>
          
          <Box sx={{ flexGrow: 0 }}>
            {
              user.nickname
              ? `${user.nickname} 님 환영합니다`
              : null
            }
          </Box>
        </Toolbar>
      </Container>
    </AppBar>
  );
}
export default Nav;

회원가입 

 

auth/register/CustomValidate.js

  • 정규표현식을 사용해 유저 입력 값을 체크
const customValidate = (type, value) => {
    if (value === ""){
        return false;
    }
    let regex = "";
    switch(type){
        case "email":
            regex = /([\w-.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/;
        case "password":
            regex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,25}$/;
        case "username":
            regex = /^[가-힣a-zA-Z0-9]+$/
        case "nickname":
            regex = /^[가-힣a-zA-Z0-9]+$/
    }
    return regex.test(value);
}  

export default customValidate;

 

auth/register/index.js

import React, { useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom";
import axios from 'axios';
import {
  Avatar,
  Button,
  CssBaseline,
  TextField,
  FormControl,
  FormHelperText,
  Grid,
  Box,
  Typography,
  Container,
} from '@mui/material/';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import validateUtil from './customValidate';
import styled from 'styled-components';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import Tooltip from '@mui/material/Tooltip';
import { VisibilityRounded } from '@mui/icons-material';

// ---------- styled components  ---------- //
const FormHelperTexts = styled(FormHelperText)`
  width: 100%;
  padding-left: 16px;
  font-weight: 700 !important;
  color: #d32f2f !important;
`;

const Boxs = styled(Box)`
  padding-bottom: 40px !important;
`;

const Register = () => {

    const endPoint = "/api/v1/user/register";
    const navigator = useNavigate();

    // ---------- states  ---------- //
    const [username, setUsername] = useState('');
    const [email, setEmail] = useState('');
    const [nickname, setNickname] = useState('');
    const [password, setPassoword] = useState('');
    const [passwordConfirm, setPassowordConfirm] = useState('');
    
    const theme = createTheme();
    const [isPasswordVisible, setIsPasswordVisible] = useState(false);  
    const [isPasswordConfirmVisible, setIsPasswordConfirmVisible] = useState(false);  
    const [isValid, setIsValid] = useState(false);
    const [registerErrorMessage, setRegisterErrorMessage] = useState('');

    // ---------- hook  ---------- //
    useEffect(()=>{
        if (email==="" || password === "" || nickname === "" || username === ""){
            setRegisterErrorMessage("");
            setIsValid(false);
        } else if (validateUtil("email", email)){
            setRegisterErrorMessage("이메일이 유효하지 않습니다.");
            setIsValid(false);
        } else if (validateUtil("password", password)){
            setRegisterErrorMessage("패스워드가 유효하지 않습니다 (숫자+영문자+특수문자 8자리 이상).");
            setIsValid(false);
        } else if (password !== passwordConfirm) {
            setRegisterErrorMessage("비밀번호와 확인값이 서로 일치하지 않습니다.")
            setIsValid(false);
        } else {
            setRegisterErrorMessage("");
            setIsValid(true);
        }
    }, [username, email, password, passwordConfirm])
    
    // ---------- handlers  ---------- //   
    const handleUsername = (e)=>{
        const input = e.target.value;
        setUsername(input);
    }
    const handleNickname = (e)=>{
        const input = e.target.value;
        setNickname(input);
    }
    const handlePassword = (e)=>{
        const input = e.target.value;
        setPassoword(input);
    }
    const handlePasswordConfirm = (e)=>{
        const input = e.target.value;
        setPassowordConfirm(input);
    }
    const handleEmail = (e)=>{
        const input = e.target.value;
        setEmail(input);
    }
    const handleIsPasswordVisible = (e)=>{
        e.preventDefault();
        setIsPasswordVisible(!isPasswordVisible)
    }
    const handleIsPasswordConfirmVisible = (e)=>{
        e.preventDefault();
        setIsPasswordConfirmVisible(!isPasswordConfirmVisible)
    }
    
    const handleSubmit = async (e) => {
        e.preventDefault();
        await axios
        .post(endPoint, {email, username, nickname, password})
        .then((res) => {                
            navigator("/login");
        })
        .catch((err) => {
            console.log(err);
            switch (err.response.data.statusCode){
                case "DUPLICATED_EMAIL":
                    setRegisterErrorMessage(email + '은 이미 존재하는 이메일입니다...');
                    break;
                case "DUPLICATED_USERNAME":
                    setRegisterErrorMessage(username + '은 이미 존재하는 유저명입니다 ...');
                    break;
                case "DUPLICATED_NICKNAME":
                    setRegisterErrorMessage(nickname + '이미 존재하는 닉네임입니다...');
                    break;
                default:
                    setRegisterErrorMessage('회원가입에 실패하였습니다...');
            }
            
        });
    };

    return (
    <ThemeProvider theme={theme}>
        <Container component="main" maxWidth="xs">
            <CssBaseline />
            <Box
                sx={{
                marginTop: 8,
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                }}>
                <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }} />
                <Typography component="h1" variant="h5">회원가입</Typography>
                <Boxs component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
                
                <FormControl component="fieldset" variant="standard">
                    <Grid container spacing={2}>
                        {/* ----- 이메일  ----- */}
                        <Grid item xs={12}>
                            <TextField
                            required
                            autoFocus
                            fullWidth
                            type="email"
                            label="이메일 주소"
                            value={email}
                            onChange={handleEmail}
                            />
                        </Grid>

                        {/* ----- 유저명 ----- */}
                        <Grid item xs={12}>
                            <Tooltip title="로그인 시 사용할 유저명을 입력해주세요">
                                <TextField
                                required
                                fullWidth
                                label="유저명"
                                value={username}
                                onChange={handleUsername}/>
                            </Tooltip>
                        </Grid>

                        {/* ----- 닉네임 ----- */}
                        <Grid item xs={12}>
                            <TextField
                            required
                            fullWidth
                            label="닉네임"
                            value={nickname}
                            onChange={handleNickname}/>
                        </Grid>

                        {/* ----- 비밀번호  ----- */}
                        <Grid item xs={10}>
                            <Tooltip title="숫자+영문자+특수문자 8자리 이상으로 작명해주세요">
                                <TextField
                                required
                                fullWidth
                                type={isPasswordVisible?"text":"password"}
                                label="비밀번호"
                                value={password}
                                onChange={handlePassword}/>
                            </Tooltip>                            
                        </Grid>
                        <Grid item xs={1}>
                            <Button onClick={handleIsPasswordVisible}>
                            {
                                isPasswordVisible?<VisibilityOffIcon/>:<VisibilityRounded/>
                            }
                            </Button>
                        </Grid>

                        {/* ----- 비밀번호 확인 ----- */}
                        <Grid item xs={10}>
                            <Tooltip title="비밀번호를 다시한번 입력해주세요">
                                <TextField
                                required
                                fullWidth
                                type={isPasswordConfirmVisible?"text":"password"}
                                label="비밀번호 재입력"
                                value={passwordConfirm}
                                onChange={handlePasswordConfirm}/>
                            </Tooltip>
                        </Grid>
                        <Grid item xs={1}>
                            <Button onClick={handleIsPasswordConfirmVisible}>
                            {
                                isPasswordConfirmVisible?<VisibilityOffIcon/>:<VisibilityRounded/>
                            }
                            </Button>
                        </Grid>
                    </Grid>

                    {/* ----- 회원가입 버튼 ----- */}
                    <Button 
                    disabled = {!isValid}
                    type="submit" 
                    fullWidth 
                    variant="contained"
                    sx={{ mt: 3, mb: 2 }} 
                    size="large">
                    회원가입
                    </Button>
                </FormControl>
                <FormHelperTexts>{registerErrorMessage}</FormHelperTexts>
                </Boxs>
            </Box>
        </Container>
    </ThemeProvider>
    );
};

export default Register;

로그인 

 

auth/login.index.js

import React, { useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom";
import axios from 'axios';
import {
  Avatar,
  Button,
  CssBaseline,
  TextField,
  FormControl,
  FormHelperText,
  Grid,
  Box,
  Typography,
  Container,
} from '@mui/material/';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import styled from 'styled-components';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import Tooltip from '@mui/material/Tooltip';
import { VisibilityRounded } from '@mui/icons-material';
import { useRecoilState } from 'recoil';
import { userState } from '../../../recoil/user';

// ---------- styled components  ---------- //
const FormHelperTexts = styled(FormHelperText)`
  width: 100%;
  padding-left: 16px;
  font-weight: 700 !important;
  color: #d32f2f !important;
`;

const Boxs = styled(Box)`
  padding-bottom: 40px !important;
`;

const Login = () => {

    const endPoint = "/api/v1/user/login";
    const navigator = useNavigate();

    // ---------- states  ---------- //
    const [username, setUsername] = useState('');
    const [password, setPassoword] = useState('');
    
    const theme = createTheme();
    const [isPasswordVisible, setIsPasswordVisible] = useState(false);  
    const [isValid, setIsValid] = useState(false);
    const [loginErrorMessage, setRegisterErrorMessage] = useState('');
    const [user, setUser] = useRecoilState(userState);

    // ---------- hook  ---------- //
    useEffect(()=>{
        if (username === ""){
            setRegisterErrorMessage("유저명을 입력해주세요");
            setIsValid(false);
        } else if (password === ""){
            setRegisterErrorMessage("패스워드를 입력해주세요");
            setIsValid(false);
        } else {
            setRegisterErrorMessage("");
            setIsValid(true);
        }
    }, [username,password])
    
    // ---------- handlers  ---------- //   
    const handleUsername = (e)=>{
        const input = e.target.value;
        setUsername(input);
    }
    const handlePassword = (e)=>{
        const input = e.target.value;
        setPassoword(input);
    }
    const handleIsPasswordVisible = (e)=>{
        e.preventDefault();
        setIsPasswordVisible(!isPasswordVisible)
    }    
    const handleSubmit = async (e) => {
        e.preventDefault();
        await axios
        .post(endPoint, {username, password})
        // 로그인 성공시
        .then((res) => {      
            const token = `Bearer ${res.data.result}`;
            // 전역변수 user 세팅
            setUser({...user, token})
            // localstorage에 token 저장
            localStorage.setItem("token", token);
            // 포스팅 페이지로 이동
            window.location.href="/post"
        })
        // 로그인 실패시
        .catch((err) => {
            console.log(err);
            switch (err.response.data.statusCode){
                case "USERNAME_NOT_FOUND":
                    setRegisterErrorMessage(username + '은 존재하지 않는 유저명입니다...');
                    break;
                case "INVALID_PASSWORD":
                    setRegisterErrorMessage('비밀번호가 틀렸습니다...');
                    break;
                default:
                    setRegisterErrorMessage('로그인에 실패하였습니다...');
            }
        });
    };

    return (
    <ThemeProvider theme={theme}>
        <Container component="main" maxWidth="xs">
            <CssBaseline />
            <Box
                sx={{
                marginTop: 8,
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                }}>
                <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }} />
                <Typography component="h1" variant="h5">로그인</Typography>
                <Boxs component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
                
                <FormControl component="fieldset" variant="standard">
                    <Grid container spacing={2}>                      

                        {/* ----- 유저명 ----- */}
                        <Grid item xs={12}>                         
                            <TextField
                            required
                            fullWidth
                            label="유저명"
                            value={username}
                            onChange={handleUsername}/>                      
                        </Grid>

                        {/* ----- 비밀번호  ----- */}
                        <Grid item xs={10}>
                            <Tooltip title="비밀번호를 입력해주세요">
                                <TextField
                                required
                                fullWidth
                                type={isPasswordVisible?"text":"password"}
                                label="비밀번호"
                                value={password}
                                onChange={handlePassword}/>
                            </Tooltip>                            
                        </Grid>
                        <Grid item xs={1}>
                            <Button onClick={handleIsPasswordVisible}>
                            {
                                isPasswordVisible?<VisibilityOffIcon/>:<VisibilityRounded/>
                            }
                            </Button>
                        </Grid>                   
                    </Grid>

                    {/* ----- 로그인 버튼 ----- */}
                    <Button 
                    disabled = {!isValid}
                    type="submit" 
                    fullWidth 
                    variant="contained"
                    sx={{ mt: 3, mb: 2 }} 
                    size="large">
                    로그인
                    </Button>
                </FormControl>
                <FormHelperTexts>{loginErrorMessage}</FormHelperTexts>
                </Boxs>
            </Box>
        </Container>
    </ThemeProvider>
    );
};

export default Login;

포스팅 List

 

post/index.js

import { Box, Container } from "@mui/system";
import axios from "axios";
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Button, Typography } from "@mui/material";
import CreateIcon from '@mui/icons-material/Create';
import DynamicFeedIcon from '@mui/icons-material/DynamicFeed';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';

const PostList = ()=>{
    const endPoint = '/api/v1/post'
    const [page, setPage] = useState(0);
    const [posts, setPosts] = useState([]);
    const navigator = useNavigate();
    
    useEffect(()=>{
        axios.get(endPoint, {
            headers:{
                Authorization:localStorage.getItem("token")
            }
        }).then((res)=>{
            return res.data.result.content
        }).then((c)=>{
            setPosts(c);
        })
        .catch((err)=>{
            console.log(err);
        })
    }, [page])

    const handleGoToWritePage = (e) =>{
        navigator("/post/write")    
    }

    return (
        <>
        <Container>

            <Box sx={{marginTop:'5vh', display:'flex', justifyContent:'space-between', alignContent:'center'}}>
                <Typography variant="h5" component="h5">
                    <DynamicFeedIcon/> 포스팅     
                </Typography>
                <Box>
                    <Button variant="contained" color="success" onClick={handleGoToWritePage} sx={{marginRight:'10px'}}>
                        <CreateIcon sx={{marginRight:'10px'}}/>포스팅 쓰기
                    </Button>
                </Box>
            </Box>
            
            {
                posts.map((p, i)=>{
                    return (
                        <Box sx={{marginTop:'5vh'}} key={i}>
                            <PostingCard post={p}/>
                        </Box>
                    )
                })
            }

        </Container>
        </>
    )
}


const PostingCard = ({post}) => {
    const endPoint = `/post/${post.id}`;
    return (
        <Card sx={{ minWidth: 275 }}>
            <CardContent>
                <CardActions sx={{justifyContent:"space-between"}}>
                    {/* 제목 */}
                    <Typography variant="h5" component="span">
                        <Link to={endPoint}> {post.title}</Link>
                    </Typography>
                    {/* 닉네임(작성자) */}
                    <Typography variant="span" component="span" color="text.secondary">
                        {post.nickname}
                    </Typography>
                </CardActions>

                <CardActions sx={{justifyContent:"space-between"}}>
                    {/* 본문 - 100글자까지만 보여주고 ... 붙이기 */}
                    <Box sx={{padding:'1vh'}}>
                        <Typography variant="body2">{post.content.slice(0, 100)}...</Typography>
                    </Box>
                    {/* 닉네임(작성자) */}
                    <Typography variant="span" component="span" color="text.secondary">
                        {post.createdAt}
                    </Typography>
                </CardActions>
                {/* TODO : 페이징 기능 */}
            </CardContent>
        </Card>
    );
  }

export default PostList;

 


포스팅 작성 

 

post/write/index.js

import axios from "axios";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import CreateIcon from '@mui/icons-material/Create';
import { Button, TextField, Typography } from "@mui/material";
import { Box, Container } from "@mui/system";
import DynamicFeedIcon from '@mui/icons-material/DynamicFeed';
import UploadIcon from '@mui/icons-material/Upload';

const WritePost = () => {
    const endPoint = "/api/v1/post";
    const navigator = useNavigate();
    // ------ state ------
    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");
    const [isLoading, setIsLoading] = useState(false);

    // ------ handler ------
    const handleTitle = (e) =>{
        setTitle(e.target.value.slice(0, 100));
    }
    const handleContent = (e) => {
        setContent(e.target.value.slice(0, 2000));
    }
    const handleGoToPostPage = (e) => {
        navigator("/post")
    }
    const handleSubmit = async (e) =>{
        e.preventDefault();
        setIsLoading(true);
        await axios.post(
            endPoint,
            {title, content},
            {
                headers:{
                    Authorization : localStorage.getItem("token")
                }
            }
        ).then((res)=>{
            navigator("/post");
        }).catch((err)=>{
            alert("포스팅 업로드에 실패하였습니다 \n" + (err.response.data.resultCode??"알수 없는 서버 오류"));
            console.log(err);
        }).finally(()=>{
            setIsLoading(false);
        });
    }
    
    return (
        <>
        <Container>

            <Box sx={{marginTop:'5vh', display:'flex', justifyContent:'space-between', alignContent:'center'}}>
                <Typography variant="h5" component="h5">
                    <CreateIcon/> 포스팅 작성하기
                </Typography>
                <Box>
                    <Button variant="contained" color="success" onClick={handleGoToPostPage} sx={{marginRight:'10px'}}>
                        <DynamicFeedIcon sx={{marginRight:'10px'}}/>포스팅 페이지로
                    </Button>
                    <Button variant="contained" color="error" type="submit" onClick={handleSubmit} disabled={isLoading}>
                        <UploadIcon sx={{marginRight:'10px'}}/>제출
                    </Button>
                </Box>
            </Box>

            <Box sx={{marginTop:'5vh'}}>
            <Box sx={{alignItems:'center'}}>
                    <Typography variant="h6" component="h6" sx={{display:'inline', marginRight:'20px'}}>제목</Typography>
                    <Typography variant="span" component="span" sx={{color:'gray'}}>
                        ({title.length} / 100)
                    </Typography>
                </Box>
                <TextField
                    sx={{width:'100%'}}
                    onChange={handleTitle}
                    variant="outlined"
                    color="warning"
                    maxRows={1}
                    value={title}
                    focused
                />  
            </Box>

            <Box sx={{marginTop:'5vh'}}>
                <Box sx={{alignItems:'center'}}>
                    <Typography variant="h6" component="h6" sx={{display:'inline', marginRight:'20px'}}>본문</Typography>
                    <Typography variant="span" component="span" sx={{color:'gray'}}>
                        ({content.length} / 2000)
                    </Typography>
                </Box>
                <TextField
                    sx={{width:'100%', height:'50vh'}}
                    onChange={handleContent}
                    variant="outlined"
                    color="warning"
                    value={content}
                    multiline
                    focused
                />
            </Box>
        </Container>
        </>
    )
}

export default WritePost;

'Java > Spring' 카테고리의 다른 글

[Spring] 간단한 SNS 만들기 #6  (0) 2023.01.15
[Spring] 간단한 SNS 만들기 #5  (0) 2023.01.15
[Spring] 간단한 SNS 만들기 #3  (0) 2022.12.22
[Spring] 간단한 SNS 만들기 #2  (0) 2022.12.22
[Spring] 간단한 SNS 만들기 #1  (0) 2022.12.22