관리 메뉴

bright jazz music

todo-react-app(front) : 완료(인증포함) 본문

Framework/ReactJs

todo-react-app(front) : 완료(인증포함)

bright jazz music 2023. 5. 26. 16:54

 

/src/index.js

// /src/index.js

import React from "react";
// import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { createRoot } from "react-dom/client";

// 라우터 경로가 들어있는 RouterApp.js파일 추가하기
import AppRouter from "./AppRouter";
/*
//기존
//ReactDOM을 사용한다.

const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
*/

const container = document.getElementById("root");
//React 18 이상 버전에서는 ReactDOM을 더이상 지원하지 않는다는 경고가 뜬다. 대신 createRoot를 사용해야 한다.
const root = createRoot(container);

root.render(<AppRouter tab="home"></AppRouter>);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

/src/App.js

// /src/App.js

import "./App.css";
import Todo from "./Todo";
import AddTodo from "./AddTodo";
import React, { useEffect, useState } from "react";
import {
  Paper,
  List,
  Divider,
  Container,
  Toolbar,
  Typography,
  AppBar,
  Button,
  Grid,
} from "@mui/material";
import { call, signout } from "./service/ApiService";

function App() {
  //기본 스테이트를 객체로 설정하였다.
  const [items, setItems] = useState([]);
  // 로딩에 사용할 스테이트 선언
  const [loading, setLoading] = useState(true);

  // //백엔드에 보낼 요청 옵션
  // const requestOptions = {
  //   method: "GET",
  //   headers: { "Content-Type": "application/json" },
  // };

  /*
  useEffect(콜백함수, 디펜던시배열) 
  :
  첫 렌더링(마운팅)이 일어났을 때, 그 이후에는 배열 안의 오브젝트 값이 변할 때마다 콜백함수를 부른다.
  따라서 렌더링 이후에 발생하는 효과인 것이다.

  빈 배열을 인자로 넘긴 이유:

  디펜던시 배열에 items를 넣었다고 가정하자. 다시 무한 루프에 빠진다.
   첫 렌더링 이후 items 내용이 바뀌고, 그 때문에 다시 렌더링이 되고 다시 useEffect가 실행되기 때문이다.
  이를 방지하기 위해 빈 배열을 넘긴 것이다.
  */
  useEffect(() => {
    //fetch를 통해 비동기 요청 발송: useEffect를 사용하지 않으면 응답이 도착한 후 setItems 때문에 무한 렌더링에 빠진다.
    console.log("App.js - useEffect-GET 아이템 리스트 가져오기");
    call("/todo", "GET", null).then((response) => {
      setItems(response.data);
      setLoading(false); //로딩 중이 아니면, 즉 값을 다 가져왔으면 loading을 false로 설정해 준다.
    });
  }, []);

  const addItem = (item) => {
    console.log("App.js - useEffect-POST 아이템 추가하기");
    call("/todo", "POST", item).then((response) => setItems(response.data));
  };

  const deleteItem = (item) => {
    console.log("App.js - useEffect-DELETE 아이템 지우기");
    call("/todo", "DELETE", item).then((response) => setItems(response.data));
  };

  /*
  const addItem = (item) => {
    item.id = "ID-" + items.length; //key를 위한 id
    item.done = false; //done 초기화. 아이템을 추가하면 기본적으로 체크박스가 체크되지 않은 상태로 추가한다.
    //업데이트는 반드시 setItems로 하고 새 배열을 만들어야 한다.
    setItems([...items, item]);
    console.log("items: ", items);
  };

  //Todo에서 쓰레기통 아이콘을 클릭하면 삭제된다. 이걸 <Todo>를 호출할 때 넣어준다. 위를 봐라.
  const deleteItem = (item) => {
    // 삭제할 item을 제외한 아이템을 다시 배열에 저장한다.
    const newItems = items.filter((e) => e.id !== item.id);
    console.log("##newItems", newItems);
    setItems([...newItems]);

    //여기가 문제네
    // const newItems = [];
    // for (let i = 0; i < items.length; i++) {
    //   if (items.id !== items[i]) {
    //     newItems.push(items[i]);
    //     // break;
    //   }
    // }
    // console.log("##newItems!!!", newItems);
    // setItems([...newItems]);
  };
*/

  /*
  //백엔드와의 통합 이후에 사용하는 아이템 수정 코드

  기존에는 Todo.js에서 값을 변경한 후 App.js에서 리스트를 재 렌더링 함으로써 리스트를 수정하였다.
  그러나 백엔드 서버와 통합하면 API를 이용하는 경우에는, 
  1. 서버 데이터를 업데이트 한 후 
  2. 변경된 내용을 화면에 다시 출력하는 작업이 필요하다.
  */
  const editItem = (item) => {
    //백엔드와 통합하기 전의 editItem 함수는 매개변수를 받지 않았다.그러나 이제부터는 수정할 item을 받아야 한다
    // 따라서 Todo.js에서 editItem()을 사용할 때 item을 넘겨줘야 한다.
    console.log("App.js : editItem() 발동");
    call("/todo", "PUT", item).then((response) => setItems(response.data));
  };

  /*
  //백엔드와의 통합 이전에 사용하던 아이템 수정 코드
  const editItem = () => {
    // items 내부의 값을 변경했기 때문에 새 배열로 초기화해 화면을 다시 렌더링한다.
    setItems([...items]);
  };
  */

  // 실제 페이지에 나타나는 게시글 영역. paper영역에 목록이 나타난다. 여기서 쓰는 props는 이 함수 이전에 선언되어야 한다.
  let todoItems = items.length > 0 && (
    <Paper style={{ margin: 16 }}>
      <List>
        {items.map((item) => (
          <Todo
            item={item}
            key={item.id}
            //삭제
            deleteItem={deleteItem}
            //수정
            editItem={editItem}
          ></Todo>
        ))}
      </List>
    </Paper>
  );

  // navigation bar(네비게이션바) 추가
  let navigationBar = (
    <AppBar position="static">
      {/* position의 역할이 무엇일까 */}
      <Toolbar>
        <Grid justifyContent="space-between" container>
          <Grid item>
            <Typography variant="h6">Test List</Typography>
          </Grid>
          <Grid item>
            <Button color="inherit" raised onClick={signout}>
              로그아웃
            </Button>
          </Grid>
        </Grid>
      </Toolbar>
    </AppBar>
  );

  // 로딩 중이 아닐 때 렌더링 할 부분
  let todoListPage = (
    <div>
      {/* 네비게이션바 + 로그아웃 */}
      {navigationBar}
      <Container maxWidth="md">
        {/* AddTodo: Todo 추가, addItem도 props로 넘긴다. */}
        <AddTodo addItem={addItem}></AddTodo>
        {/* Todo 목록 표시 */}
        {todoItems}
      </Container>
    </div>
  );

  // 로딩 중일 때 렌더링 할 부분
  let loadingPage = <h1> 로딩 중... </h1>;
  let content = loadingPage;

  if (!loading) {
    // loading 스테이트가 false라면, 즉 로딩 중이 아니라 이미 값을 가져온 상태라면 todoList를 선택
    content = todoListPage;
  }

  // 선택한 content 렌더링
  return <div className="App">{content}</div>; //기본적으로는 로딩중인 화면을 반환

  // return (
  //   <div className="App">
  //     {/* 네비게이션바 + 로그아웃 */}
  //     {navigationBar}
  //     <Container maxWidth="md">
  //       {/* AddTodo: Todo 추가, addItem도 props로 넘긴다. */}
  //       <AddTodo addItem={addItem}></AddTodo>
  //       {/* Todo 목록 표시 */}
  //       {todoItems}
  //     </Container>
  //   </div>
  // );
}

export default App;

 

/src/Todo.js

// /src/Todo.js

import { CheckBox } from "@mui/icons-material";
import {
  InputBase,
  ListItem,
  ListItemText,
  Checkbox,
  Divider,
  ListItemSecondaryAction,
  IconButton,
} from "@mui/material";
// import { DeleteOutlined } from "@mui/icons-material/DeleteOutlined";
import { DeleteOutline } from "@mui/icons-material/";
import React from "react";
import { useState } from "react";

const Todo = (props) => {
  // function Todo(props) {
  const [item, setItem] = useState(props.item);
  const deleteItem = props.deleteItem;
  const editItem = props.editItem;

  const [readOnly, setReadOnly] = useState(true);

  // 아이템 삭제를 위한 함수 <IconButton> 에서 작동한다.
  const deleteEventHandler = () => {
    deleteItem(item);
  };

  /*
  타이틀 변경을 위해 인풋필드에서 사용자의 입력을 받아올 때, 
  editEventHandler()에서 item을 바로 넘겨버리면 글자를 입력할 때마다 HTTP 요청을 보내게 된다. X
  사용자가 수정을 완료한 시점에서 http 요청을 보내야 한다. O
  수정을 완료한 시점은 인풋필드가 수정 가능한 상태에서 수정이 불가능한 상태로 바뀌는 시점이다.
  
  따라서 editEventHandler에서는 프론트엔드의 item 값만 업데이트하고, editItem()은 생략하여 HTTP 요청은 보내지 않는다.
  이후 사용자가 엔터키를 누르는 순간 실행되는 turnOnReadOnly()에서 HTTP요청을 보내는 editItem()을 실행한다.

   */
  /*
  const editEventHandler = (event) => {
    //이벤트의 변화가 props.item.title을 변경시킨다.
    item.title = event.target.value;
    console.log("##event.item.title", item.title);
    //App.js의 editItem()를 실행한다. setItems([...items])를 통해 재 렌더링하게 하여 바뀐 item 내용을 표시하도록 한다.
    editItem(); //HTTP 요청을 보내지 않도록 주석처리. 이 함수를 입력이 끝나는 시점인 turnOnReadOnly 함수에서 실행한다.
  };
*/

  const editEventHandler = (event) => {
    setItem({ ...item, title: event.target.value });
  };

  //turnOffReadOnly 함수 작성
  const turnOffReadOnly = () => {
    setReadOnly(false);
  };

  const turnOnReadOnly = (event) => {
    if (event.key === "Enter") {
      setReadOnly(true);
      //editEventHandler가 setItem()을 사용하여 변경한 item을 매개변수로 담아서 실행한다.
      //App.js의 editItem()이 호출된다.
      editItem(item);
    }
  };

  const checkBoxEventHandler = (event) => {
    item.done = event.target.checked;
    console.log("##item.done", item.done);
    // editItem();
    editItem(item); //변경된 체크 정보가 적용된 item을 담아서 editItem() 실행
  };

  return (
    <ListItem>
      {/** 체크박스: Box가 대문자가 아님에 주의. item.done의 기본값은 false. 체크하면 true로 바뀜. */}
      <Checkbox checked={item.done} onChange={checkBoxEventHandler}></Checkbox>
      {/* 체크박스 옆 텍스트 영역 */}
      <ListItemText>
        <InputBase
          inputProps={{ "aria-label": "naked", readOnly: readOnly }}
          //클릭하면 ReadOnly가 false로 바뀌면서 커서 점멸
          onClick={turnOffReadOnly}
          //Enter키 누르면 turnOnReadOnly 실행: ReadOnly가 true로 바뀜
          onKeyDown={turnOnReadOnly}
          //여기서의 변화가 event에 editEventHandler 함수로 전달
          onChange={editEventHandler}
          type="text"
          id={item.id}
          name={item.id}
          value={item.title}
          multiline={true}
          fullWidth={true}
        ></InputBase>
      </ListItemText>

      {/* 삭제버튼을 위한 구분 */}
      <ListItemSecondaryAction>
        {/* 아이콘 버튼에 함수를 연결한다. */}
        <IconButton aria-label="Delete Todo" onClick={deleteEventHandler}>
          {/* 삭제 아이콘 버튼*/}
          <DeleteOutline></DeleteOutline>
        </IconButton>
      </ListItemSecondaryAction>
    </ListItem>
  );

  //   return (
  //     <div className="Todo">
  //       <input type="checkbox" id={item.id} name={item.id} checked={item.done} />
  //       <label id={item.id}>{item.title}</label>
  //     </div>
  //   );
};

export default Todo;

 

/src/AddTodo.js

// /src/AddTodo.js

import React, { useState } from "react";
import { Button, Grid, TextField } from "@mui/material";

// function AddTodo(props) {
const AddTodo = (props) => {
  //사용자의 입력을 저장할 오브젝트
  const [item, setItem] = useState({ title: "" });
  const addItem = props.addItem;

  //onInputChange 함수 작성
  const onInputChange = (event) => {
    setItem({ title: event.target.value });
    console.log("##AddTodo-item", item);
  };

  const onButtonClick = () => {
    console.log("##AddTodo: onButtonClick 발동");
    addItem(item);
    // item을 다시 초기화 해준다. 이렇게 해주지 않으면 방금 추가된 내용이 item에 남기 때문이다.
    setItem({ title: "" });
  };

  //enterKeyEventHandler 함수: Enter키를 누르면 발동됨다
  const enterKeyEventhandler = (event) => {
    if (event.key === "Enter") {
      onButtonClick(); //괄호를 빼먹지 않도록 주의
    }
  };

  return (
    <Grid container style={{ marginTop: 20 }}>
      <Grid xs={11} md={11} item style={{ paddingRight: 16 }}>
        <TextField
          placeholder="Add Todo here"
          fullWidth
          onChange={onInputChange}
          //엔터키 입력 시 onKeyPress 발동
          onKeyPress={enterKeyEventhandler}
          value={item.title}
        ></TextField>
      </Grid>
      <Grid xs={1} md={1} item>
        <Button
          fullWidth
          style={{ height: "100%" }}
          color="secondary"
          variant="outlined"
          // 버튼클릭 시 onClick 발동 App.js addItem() 실행
          onClick={onButtonClick}
        >
          +
        </Button>
      </Grid>
    </Grid>
  );
};

export default AddTodo;

 

/src/api-config.js

// api-config.js

let backendHost;
const hostname = window && window.location && window.location.hostname;

if (hostname === "localhost") {
  backendHost = "http://localhost:8080";
}

export const API_BASE_URL = `${backendHost}`;

 

/src/SignUp.js

// /src/SignUp.js

import React from "react";
import { Container, Grid, Typography, TextField, Button } from "@mui/material";
import { signup } from "./service/ApiService";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";

// 이 페이지를 작성하였으면 AppRouter.js의 라우터를 추가해주고, 그 다음엔 Login.js에 계정생성으로 이동하는 링크를 추가해준다.

function SignUp() {
  const handleSubmit = (event) => {
    event.preventDefault();
    // 오브젝트에서 form에 저장된 데이터를 맵의 형태로 바꿔준다.
    const data = new FormData(event.target);
    const username = data.get("username");
    const password = data.get("password");

    //백엔드 서버로 객체 전달
    signup({ username: username, password: password }).then((response) => {
      // 계정 생성 성공 시 login 페이지로 리다이렉트
      window.location.href = "/login";
    });
  };

  return (
    <Container component="main" maxWidth="xs" style={{ marginTop: "8%" }}>
      <Grid container spacing={2}>
        <Grid item xs={12}>
          <Typography component="h1" variant="h5">
            계정 생성
          </Typography>
        </Grid>
      </Grid>
      <form noValidate onSubmit={handleSubmit}>
        {" "}
        {/* submit 버튼을 누르면 handleSubmit이 실행된다 */}
        <Grid container spacing={2}>
          {/* 아이디 입력창 */}
          <Grid item xs={12}>
            <TextField
              variant="outlined"
              required
              fullWidth
              id="username"
              label="아이디"
              name="username"
              autoComplete="username"
            ></TextField>
          </Grid>

          {/* 비밀번호 입력창 */}
          <Grid item xs={12}>
            <TextField
              variant="outlined"
              required
              fullWidth
              id="password"
              label="패스워드"
              name="password"
              autoComplete="current-password"
            ></TextField>
          </Grid>
          <Grid item xs={12}>
            <Button type="submit" fullWidth variant="contained" color="primary">
              계정 생성
            </Button>
          </Grid>
        </Grid>
        {/* 버튼 아래 계정 보유 여부 확인란 */}
        <Grid container justify="flex-end">
          <Grid item mt={2}>
            <Link to="/login" variant="body2">
              이미 계정이 있습니까? 로그인 하세요.
            </Link>
          </Grid>
        </Grid>
      </form>
    </Container>
  );
}

export default SignUp;

 

/src/Login.js

// /src/Login.js : 로그인을 위한 폼이 존재하는 페이지
import React from "react";
import { signin } from "./service/ApiService";
import { TextField, Typography, Container, Grid, Button } from "@mui/material";
import { Link } from "react-router-dom";

function Login() {
  //폼의 submit 컨트롤
  const handleSubmit = (event) => {
    event.preventDefault();
    const data = new FormData(event.target);
    const username = data.get("username");
    const password = data.get("password");

    // /src/ApiService.js의 signin() 메서드를 사용해 로그인
    signin({ username: username, password: password });
  };

  return (
    <Container component="main" maxWidth="xs" style={{ marginTop: "8%" }}>
      <Grid container spacing={2}>
        <Grid item xs={12}>
          <Typography component="h1" variant="h5">
            로그인
          </Typography>
        </Grid>
      </Grid>
      <form noValidate onSubmit={handleSubmit}>
        {" "}
        {/* submit 버튼을 누르면 handleSubmit이 실행된다 */}
        <Grid container spacing={2}>
          {/* 아이디 입력창 */}
          <Grid item xs={12}>
            <TextField
              variant="outlined"
              required
              fullWidth
              id="username"
              label="아이디"
              name="username"
              autoComplete="username"
            ></TextField>
          </Grid>

          {/* 비밀번호 입력창 */}
          <Grid item xs={12}>
            <TextField
              variant="outlined"
              required
              fullWidth
              id="password"
              label="패스워드"
              name="password"
              autoComplete="current-password"
            ></TextField>
          </Grid>
          <Grid item xs={12}>
            <Button type="submit" fullWidth variant="contained" color="primary">
              로그인
            </Button>
          </Grid>
        </Grid>
        {/* 계정 생성 여부 확인란 */}
        <Grid item mt={2}>
          <Link to="/signup" variant="body2">
            계정이 없습니까? 여기서 확인하세요
          </Link>
        </Grid>
      </form>
    </Container>
  );
}

export default Login;

 

/src/AppRouter.js

// /src/AppRouter.js : 페이지 라우팅 정보를 적는 페이지
import React from "react";
import "./index.css";
import App from "./App";
import Login from "./Login";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Typography, Box } from "@mui/material";
import SignUp from "./SignUp";

function Copyright() {
  return (
    <Typography variant="body2" color="textSecondary" align="center">
      {"Copyright © "}
      fsoftwareengineer, {new Date().getFullYear()}
      {"."}
    </Typography>
  );
}

function AppRouter() {
  return (
    <div>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<App />}></Route>
          <Route path="login" element={<Login />}></Route>
          {/* 계정생성 라우터 추가. 이후 Login.js 컴포넌트에 SignUp.js로 가능 링크를 추가해준다. */}
          <Route path="signup" element={<SignUp />}></Route>
        </Routes>
      </BrowserRouter>
      <Box mt={5}>
        <Copyright />
      </Box>
    </div>
  );
}

export default AppRouter;

 

/src/service/ApiService.js

// /service/ApiService.js

import { API_BASE_URL } from "../api-config";

export function call(api, method, request) {
  let headers = new Headers({
    "Content-type": "application/json",
  });
  //   alert(headers);

  //로컬 스토리지에서 ACCESS TOKEN 가져오기
  const accessToken = localStorage.getItem("ACCESS_TOKEN");
  if (accessToken && accessToken != null) {
    headers.append("Authorization", "Bearer " + accessToken);
  }

  let options = {
    headers: headers,
    // http://localhost:8080 + api
    url: API_BASE_URL + api,
    method: method,
  };

  //   let options = {
  //     headers: new Headers({
  //       "Content-type": "application/json",
  //     }),
  //     url: API_BASE_URL + api,
  //     method: method,
  //   };

  if (request) {
    //GET method
    options.body = JSON.stringify(request);
  }

  //   return fetch(options.url, options)
  //     .then((response) => {
  //       if (response.status === 200) {
  //         return response.json();
  //       }
  //     })
  //     .catch((error) => {
  //       console.log("http error");
  //       console.log(error);
  //     });
  // 로그인 하지 않았을 경우 로그인 페이지로 리다이렉트 해주는 로직을 넣어줘야 한다.

  return fetch(options.url, options)
    .then((response) => {
      if (response.status === 200) {
        return response.json();
      } else if (response.status === 403) {
        window.location.href = "/login"; //로그인 페이지로 redirect
      } else {
        Promise.reject(response);
        throw Error(response);
      }
    })
    .catch((error) => {
      console.log("http error");
      console.log(error);
    });
}

//로그인 시 사용하는 함수
export function signin(userDTO) {
  return call("/auth/signin", "POST", userDTO).then((response) => {
    console.log("response: ", response);
    if (response.token) {
      alert("로그인 토큰: " + response.token);
      //로컬 스토리지에 토큰 저장
      localStorage.setItem("ACCESS_TOKEN", response.token);
      //토큰이 존재하는 경우 "/"로 리다이렉트
      window.location.href = "/";
    }
  });
}

//로그아웃 시 사용하는 함수
export function signout() {
  localStorage.setItem("ACCESS_TOKEN", null);
  window.location.href = "/login";

  //로그아웃 버튼은 App.js 컴포넌트에 네비게이션 바를 추가하고 거기에 링크를 추가한다.
}

//계정 생성 시 사용하는 함수
export function signup(userDTO) {
  return call("/auth/signup", "POST", userDTO);
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'Framework > ReactJs' 카테고리의 다른 글

useContext 훅  (0) 2023.06.16
[Do it! w. TS] 03. 가상 DOM 이해하기  (0) 2023.06.15
todo-java  (0) 2023.05.24
todo-react-app(front) : fetch 이후 CORS 오류, useEffect, fetch, Promise  (0) 2023.05.24
todo-react-app(front 예전)  (0) 2023.05.23
Comments