리액트 - 달력/TodoList

정영찬·2022년 4월 10일
0

프로젝트 실습

목록 보기
5/60
post-thumbnail

달력과 TodoList가 합쳐진 페이지를 제작한다.

기능

  • 메인화면에서 달력화면과 남은 할일의 갯수와 함께 할일의 목록을 나타내는 리스트 페이지가 렌더링된다.

  • 달력에는 년월일 과 함께 현재 날짜에 하이라이트가 되어있고, 날짜를 클릭하면 해당 날에 적힌 TodoList가 오른쪽에 나타나게 된다.

  • TodoList 컴포넌트는 추가 버튼을 누르면 입력창이 나타나고,사용자가 입력하고 추가하기를 누르면 리스트에 할일이 추가된다.

헤더

왼쪽에는 사이트의 제목과 오른쪽에는 간단한 문구(As always, you can do it.)가 나타나게 한다. material-ul 컴포넌트를 import 해서 구현했다.

components/Header/Header.js

import React, { useState } from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import useStyles from './styles'





export default function Header() {
  const classes = useStyles();
  const [checked, setChecked] = useState(true);

  const viewChange = () => {
    setChecked((prev) => !prev);
  };

  return (
    <Box sx={{ flexGrow: 1 }}>
      <AppBar  position="static">
        <Toolbar className={classes.toolbar}>
          <Typography variant="h3" component="div" sx={{fontSize: {xs:'20px',md:'50px'},transition:'0.5s'}} >
            Todo-Cal
          </Typography>
          <Typography  sx={{opacity: {xs:'0', md:'1'}, transition: '0.5s'}} variant="h3" component="div" >
            As always, you can do it
          </Typography>
        </Toolbar>
      </AppBar>
    </Box>
  );
}

기준으로 정한 너비보다 줄어들경우 오른쪽의 문구가 사라지고 왼쪽 로고의 폰트 크기를 줄어들게 제작한다.

Content

왼쪽에는 달력이, 오른쪽에는 투두리스트가 나타난다. 달력은 react-datepicker를 사용했다.
오른쪽에는 투두 리스트가 배치된다.

import { styled } from "@mui/material/styles";
import { Grid, Paper} from "@mui/material";
import DatePicker from "react-datepicker";
import React, { useState } from "react";
import "react-datepicker/dist/react-datepicker.css";
import { TodoProvider } from "./TodoContext";
import TodoHead from "./TodoList/TodoHead";
import TodoLists from "./TodoList/TodoLists";

const Item = styled(Paper)(({ theme }) => ({
  backgroundColor: theme.palette.mode === "dark" ? "#1A2027" : "#fff",
  ...theme.typography.body2,
  padding: theme.spacing(1),
  textAlign: "center",
  color: theme.palette.text.secondary,
  height: "700px",
  display: "flex",
  alignItems: "center",
  justifyContent: "center",
  position: "relative",
  border: "1px solid black",
}));

const Content = () => {
  const [value, setValue] = useState(new Date());

  console.log(value.getDate(), value.getMonth() + 1);


  return (
    <Grid container spacing={2}>
      <Grid item xs={12} md={6}>
        <Item>
          <DatePicker
            selected={value}
            onChange={(date) => setValue(date)}
            inline
          />
        </Item>
      </Grid>
      <Grid item xs={12} md={6}>
        <Item sx={{ display: "flex", flexDirection: "column" }}>
          <TodoProvider>
            <TodoHead value={value}/>
            <TodoLists/>
          </TodoProvider>
        </Item>
      </Grid>
    </Grid>
  );
};

export default Content;

투두 리스트 컴포넌트

투두 리스트는 따로 폴더를 만들어서 컴포넌트를 따로 만들어서 제작한다.

  • TodoHead: 처음 들어갈 때는 현재 날짜를 표시하고 달력의 날짜를 선택하면 그에 맞는 날짜와 요일이 나타난다. 또한 초기 데이터에 내장된 할일 목록중 체크가 되지 않은 목록의 갯수를 표시한다.
  • TodoList: TodoItem 이 반복적으로 출력된다. 하단에 버튼을 만들었고, 클릭하면 할일을 입력하는 input 창이 나타난다.
  • TodoItem : checkbox 아이콘, 할일의 내용, 삭제 아이콘이 들어있으며 삭제아이콘은 마우스커서가 올라와있을 때에만 보여지게 구현한다.

TodoHead

import { Typography } from '@mui/material';
import React from 'react'
import { useTodoState } from '../TodoContext'

const TodoHead = ({value}) => {
    const todos = useTodoState();
    const undoneTasks = todos.filter(todo => !todo.done)
    console.log({value})
    const dateString = value.toLocaleDateString('ko-KR',{
        year: 'numeric',
        month: 'long',
        day: 'numeric'
    });
    const dayName = value.toLocaleDateString('ko-KR', {
        weekday: 'long'
    });
  return (
    <div>
      <Typography variant="h4">{dateString}</Typography>
      <Typography variant="h5">{dayName}</Typography>
      <Typography variant="h6">남은 일 갯수 {undoneTasks.length}</Typography>
    </div>
  )
}

export default TodoHead;

TodoItem

import { Checkbox, IconButton, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
import React from 'react'
import { useTodoDispatch } from '../TodoContext';
import DeleteIcon from "@mui/icons-material/Delete";

function TodoItem({id,done,text}){
    const dispatch = useTodoDispatch();
    const onToggle = () => dispatch({
        type: 'TOGGLE',
        id
    });

    const onRemove = () => dispatch({
        type: 'REMOVE',
        id
    })
  
   
  return (
   <>
   <ListItem  
              sx={{width:'100%'}}  
              key={id}
              id={id}
              onClick={onToggle}
              secondaryAction={
                <IconButton
                  edge="end"
                  aria-label="delete"
                  onClick={onRemove}
                  sx={{
                    opacity: "0",
                    transition: "0.5s",
                    "&:hover": { opacity: "1" },
                  }}
                >
                  <DeleteIcon />
                </IconButton>
              }
              disablePadding
            >
              
              <ListItemButton
                role={undefined}
                dense
                sx={{ border: "1px solid black" }}
              >
                <ListItemIcon  >
                  <Checkbox
                    edge="start" 
                    done={`${done}`}
                    checked={done}
                    disableRipple
                  />
                </ListItemIcon>
                <ListItemText
                    disableTypography
                    sx={{
                       fontSize:'30px'
                      }}
                >{text}</ListItemText>
              </ListItemButton>
            </ListItem>
   </>
  )
}

export default TodoItem

TodoList

import { Button, InputBase, List, Paper } from "@mui/material";
import React, { useState } from "react";
import { useTodoDispatch, useTodoNextId, useTodoState } from "../TodoContext";
import AddIcon from "@mui/icons-material/Add";
import TodoItem from "./TodoItem";
const TodoLists = () => {
  const todos = useTodoState();
  const dispatch = useTodoDispatch();
  const nextId = useTodoNextId();
  const [open, setOpen] = useState(false);
  const onOpen = () => setOpen(!open);

  const [value,setValue] = useState('');

  const onChange = (e) => setValue(e.target.value);

  const onSubmit = () => {
      dispatch({
          type:'CREATE',
          todo: {
              id: nextId.current,
              text: value,
              done: false,
          }
      });
      setValue('');
      setOpen(false);
      nextId.current += 1;
  }

  return (
    <div>
      <List sx={{ width: "500px", maxWidth: '500px', bgcolor: "background.papaer",maxheight:'500px' }}>
        {todos.map((todo) => (
          <TodoItem
            key={todo.id}
            id={todo.id}
            text={todo.text}
            done={todo.done}
          />
        ))}
      </List>
      {open ? (
          <>
            <Paper
              component="form"
              sx={{
                display: "flex",
                width: 400,
                position:'absolute',
                bottom:'50px',
                left:'50%',
                transform:'translate(-50%,-50%)',

               
              }}
            >
              <InputBase
                sx={{ ml: 1, flex: 1 }}
                placeholder="뭐든 좋으니깐요"
              
                onChange={onChange}
                inputProps={{ style: { textAlign: "center" } }}
              />
            </Paper>
            <Button
              onClick={onSubmit}
              variant="contained"
              sx={{
                position:'absolute',
                bottom:'0',
                left:'50%',
                transform:'translate(-50%,-50%)',
              }}
            >
              ADD
            </Button>
          </>
        ) : (
          <Button
            onClick={onOpen}
            variant="contained"
            endIcon={<AddIcon />}
            sx={{
                position:'absolute',
                bottom:'0',
                left:'50%',
                transform:'translate(-50%,-50%)',
            }}
          >
            ADD LIST
          </Button>
        )}
    </div>
  );
};

export default TodoLists;


데이터가 추가된 모습이다.

localstorage를 사용한 데이터 변경

하고싶은거 : 달력의 날짜를 클릭하면 해당 날짜로 지정된 데이터를 바탕으로 TodoList가 렌더링되게 한다.

localstorage에 데이터를 저장하기

먼저 사용자가 할일을 작성하고 난 다음에 ADD버튼을 누르면 데이터가 localstorage에 저장되게 해야한다.
TodoItem을 반복적으로 렌더링 하는 컴포넌트인 TodoList의 코드를 수정했다.

먼저 Todo_Data 라는 key 값을 가진 localStrage를 생성하고 그 배열안에 사용자가 작성한 내용을 push한 뒤에 다시 setItem메소드를 사용해서 저장한다.

var currentData = JSON.parse(localStorage.getItem('Todo_Data') || '[]')

이게 그냥 빈배열 []로 덩그러니 해놓으니까 초기화가 되어서 Todo_Data내부에 데이터가 있으면 그 내용을 불러오고, 아니면 빈 배열을 불러오는 것으로 작성했다.

사용자가 내용을 작성한뒤 추가 버튼을 누르면 호출되는 onSubmit함수의 내용을 수정했다.
currentData에 추가되는 data객체를 제작했다.

 const data = {
        id:nextId.current,
          text:value,
          done:false,
          day: day,
          month: month,
    } 

여기서 daymonthContent.js에서 사용자가 달력을 선택할때 date 값을 TodoLists에게 props로 전달한 것이다.

datacurrentdata에게 추가 한다음에 localStorge Todo_Data의 내용을 최신화 시킨다.

 currentData.push(data)
 localStorage.setItem('Todo_Data', JSON.stringify(currentData));
      

현재 상황

localStorage에 데이터가 성공적으로 추가된 모습이다.

그리고 TodoContext의 초기 전송 데이터인 initialTodos의 값을 수정했다. localStorage에 있는 데이터들을 초기값으로 지정했다.
TodoContext

const Datas = JSON.parse(localStorage.getItem('Todo_Data') || '[]');
const initialTodos = Datas.map((data,index) => {
     
   return data;

})

또한 사용자가 할일을 추가 할 경우 이전에는 초기값을 인위적으로 4개를 만들었기 때문에 변수 nextId값을 useRed(5)로 지정했으나 아예 없는 경우부터 시작해야 하므로

해당 날짜에 따른 투두리스트를 불러오기

사용자가 달력의 날짜를 눌렀을때 그에 해당하는 localStorage의 데이터를 불러와서 투두리스트를 렌더링한다. TodoLists에 props로 전송한 date데이터를 사용해서 코드를 수정할 것이다.

이전에는 TodoContext에서 initialState 의 전체 값을 토대로 TodoItem을 렌더링했기 때문에 모든 데이터가 들어가게 된다.

나는 그 데이터는 todos라는 변수로 선언해서 불러왔고, map함수를 사용해서 해당 데이터의 수만큼 반복적으로 렌더링을 했었다.

그렇다면 todos에서 특정 조건을 만족한 데이터의 배열을 따로 생성한뒤 그 배열을 todos대신에 사용한다면, 사용자가 클릭한 날짜에 따른 데이터가 나오지 않을까..?

그래서filter사용해서 todos의 daymonth데이터가 props로 받아온 daymonth가 일치하는 데이터만 모여있는 배열을 만들었다.

const selectedDateData = todos.filter(
    todos =>  todos.day === day && todos.month === month
   ) 

그리고 return 문에서 map 함수에 사용된 todos대신에 selectedDateData를 사용했다.

{selectedDateData.map((todo) => (
          <TodoItem
            key={todo.id}
            id={todo.id}
            text={todo.text}
            done={todo.done}
            date={date}
          />
        ))}

현재 상황

선택한 날짜에 따라 저장된 투두 리스트가 나타난다.

문제 상황1: 바뀐 날짜에서 리스트 작성시 id가 초기화되지 않음

오늘 리스트 2개를 작성하고 다른 날짜에서 리스트를 작성하면 id값이 1이 아니라 3부터 작성된다.
TodoContext에서 가져온 nextId가 항상 0에서부터 시작했기 때문에 초기화가 되지 않은 것이라고 생각한다. 그렇다면 만약 해달 날짜의 데이터 길이에 1을 더한 값을 nextId로 지정하면 되지 않을까?

 var nextId = parseInt(selectedDateData.length) + 1

onSubmit 함수에서 data객체 내부에서 id 값을 수정한다.

 const data = {
        id:nextId,
          text:value,
          done:false,
          day: day,
          month: month,
    } 

dispatch에서 CREATE type의 경우에 todo 데이터 값중에서 id 값을 수정했다.

dispatch({
          type:'CREATE',
          todo: {
              id: nextId,
              text: value,
              done: false,
              day:day,
              month:month,
          }
      });

그리고 locatSotrage에 데이터를 저장한뒤 nextId 값을 1 더해주는것도 잊지말자.

nextId = nextId + 1 

현재 상황

날짜가 달라도 데이터를 추가하면 1부터 시작하는 모습을 볼수 있다. 야호!

문제 상황 2 : 데이터 삭제 문제

리스트의 데이터를 하나만 지웠는데도 localStorage의 데이터가 몽땅 사라진다. 페이지에서는 리스트를 삭제 버튼을 누르면 하나만 지워진 상태로 보이지만 localStorage 데이터는 모두 제거된다.

원인을 발견한건 contextAPI 에서 액션 타입 코드였다. TodoContext.js에서 action.type 중에서 REMOVE 의 코드를 filter 함수에서 todo.idaction.id로만 비교해서 데이터를 분류했기 때문에 월,일이 다름에도 불구하고 id 값이 같기 때문에 다같이 지워진 것이었다.

따라서

return state.filter(
                todo => {
                    return(
                        todo.id !== action.id 
                    )
                }   
            );

위의 내용을

 return state.filter(
                todo => {
                    return(
                        todo.id !== action.id ||
                        ( todo.day !== action.day || todo.month !== action.month)
                    )
                }   

이렇게 수정해서 세부조건을 부여해서 설령 id 값이 같다고 하더라도 일, 월이 다르다면 지워지지 않게 해주는 것이다.

그리고 state에게 전해주는 데이터를 id로만 전송시켜서 이렇게 조건문을 적었어도 적용되지않았다. 따라서 TodoItemonRemove에서 사용하는 dispatch 내용을 수정해야한다.

 dispatch({
        type: 'REMOVE',
        id,
        day,
        month,
        })

이런식으로 day 값과 month값을 전송해서 state에서도 활용이 가능하게 했다.

결과

현재 4월10일과 5월 10일에같은 id값의 리스트가 존재하지만 삭제를 해도 5월 10일의 데이터는 그대로 남아있게되었다.

문제 상황 3 : Toggle 문제

Toggle 또한 id 값으로만 받아와서 그런지 원하는데로 checkbox의 값이 변경되지 않았었고, 해결 방법도 비슷했다. Toggle타입의 dispatch내용을 수정했다.

 dispatch({
        type: 'TOGGLE',
        id,
        day,
        month
      }) 

TodoContext.js에서도 마찬가지로 액션 타입 TOGGLE 타입의 경우 map 함수 내용을 추가한다.
id 값이 같고, day, month, 값이 모두 일치 해야 check가 되어야하는것이 올바르기 때문에

case 'TOGGLE':
            return state.map(
                todo => todo.id === action.id &&
                 todo.day === action.day && 
                 todo.month === action.month  ? {...todo, done: !todo.done}:todo,
               
                );

이런식으로 3가지의 조건이 모두 true 가 아니면 false로 하여금 그값 그대로 반환하게 해주는 것이다.

현재 상황

4월 10일 id값이 1인 항목을 체크해도 5월 10일 id값이 1인 항목에 체크되지 않은 모습을 볼수 있다.

그리고 이 문제를 해결하기 이전에
사용자가 checkBox를 체크하면 localStorage에도 그 값이 변경된 상태로 저장하는 코드를 작성했다.

TodoItem.js 에서 onToggle 의 내용을 추가한다.
currentData는 localStorage에서 현재 저장 되어있는 데이터를 불러온것이다.


      var toggledDataset = currentData.map(
        seltodo => {
          return ((
            seltodo.id === selectedId &&
            seltodo.day === day &&
            seltodo.month === month 
          )? {...seltodo, done: !seltodo.done}: seltodo)
        }
      )

      localStorage.setItem('Todo_Data', JSON.stringify(toggledDataset));
      
      };

TodoContext에서 작성한것과 같은 조건문을 작성하고, 조건을 만족하는 데이터가 담긴 toggledDataset을 선언한뒤 localStorage.setItem으로 수정한 내용을 업데이트 시켰다.

이 작업을 진행하지 않으면, 시각적으로는 체크가 되었을지 모르지만 새로고침을 하면 localStorage에서는 done값이 변경되지 않았기 때문에 변경 되지 이전의 상태로 돌아오게 된다.

이로써 최종적으로 완전히 기능이 작동되는 달력과 연동된 투두리스트 가 완성되었다. react date-picker의 스타일 수정의 방법을 몰라서 저렇게 기본형으로 놔둔 것이 조금 아쉽지만 그래도 기능은 제대로 동작하니 나중에 달력을 따로 만들어도 같은 방식으로 데이터 전송만 한다면 아무 문제없이 수정이 가능 할 것이다.

만들어 보고 싶었던 프로젝트라서 완성되고 나니 매우 뿌듯하다. 비록 대단한 것이 아닐지언정 조금씩 성장하고 있다는 게 아닐까?... 아닌가?

수정

최종 점검을 하다가 보니까 data set에 년도가 추가되어있지 않아서 추가하려고 한다. 그렇게 대단할 건 없고 그냥 데이터 객체항목에 year를 추가하면 된다.

TodoList.js onSubmit 수정

 const data = {
      id: nextId,
      text: value,
      done: false,
      day: day,
      month: month,
      year: year
    };
 dispatch({
      type: "CREATE",
      todo: {
        id: nextId,
        text: value,
        done: false,
        day: day,
        month: month,
        year: year
      },
    }); 

TodoItems.js onToggle, onRemove 수정

dispatch({
        type: 'TOGGLE',
        id,
        day,
        month,
        year
      }) 
     
 dispatch({
        type: 'REMOVE',
        id,
        day,
        month,
        year
        })

적용 후 화면

데이터 셋에 year 까지 추가된 모습이다. 확실히 어떻게 돌아가는지 파악이 되니까 수정하는것도 난이도가 눈에 띄게 줄어들었다. 더 열심히 해야지.

profile
개발자 꿈나무

0개의 댓글