본문 바로가기
프론트엔드

[Next.js, React] TodoList 만들어보기

by goodchuck 2024. 6. 4.

목차

     

     

     폴더구조

     

    Next.js를 사용하였으며 /todolist 경로 이동시 사용할 page.tsx와 

    containers/TodoList를 사용하였다.

     

     

     파일 내용

     

    /src/app/todolist/page.tsx

    import TodoList from '@/containers/TodoList/TodoList';
    
    export default function Page() {
      return <TodoList />;
    }

    /todolist 경로에서 보여줄 컨테이너

     

    /src/containers/TodoList/TodoList.styles.ts

    import styled from 'styled-components';
    
    const StyledTodoList = styled.div`
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      height: 100vh;
    
      .TodoList__wrapper {
        border: 1px solid black;
        padding: 10px;
      }
    
      .TodoList__cancel {
        text-decoration: line-through;
      }
    `;
    
    export default StyledTodoList;

    TodoList 컨테이너의 스타일을 담당할 파일

     

    /src/containers/TodoList/TodoList.tsx

     

     선언부

    'use client';
    
    import { useState, ChangeEvent } from 'react';
    import { v4 as uuidv4 } from 'uuid';
    import StyledTodoList from './TodoList.styles';
    
    type StatusType = '미시작' | '진행중' | '완료';
    
    type SortType = 'Ascending' | 'Descending';
    
    interface Todo {
      id: number;
      text: string;
      status: StatusType;
      date: Date;
    }
    
    const INITIAL_TODOS: Todo[] = [];
    const SORT_TEXT = {
      Ascending: '오름차순',
      Descending: '내림차순',
    };
    • use client : useState등 클라이언트 컴포넌트에만 사용가능한 함수가 있기 때문에 클라이언트 컴포넌트 선언을 위해 사용
    • uuid : Array.map으로 반환되는 react컴포넌트의 고유한 Key를 제공하기위해 사용
    • StyledTodoList : TodoList의 styled-components 파일
    • type StatusType : 할일마다 dropdown으로 지정하기 위한 타입 선언
    • type SortType : 오름차순, 내림차순을 적용하기위한 선언
    • interface Todo : 할일의 인터페이스 선언
    • INITIAL_TODOS  : 할일 배열의 초기값 선언
    • SORT_TEXT : 영어로 되어있는 SortType의 매치되는 한글 선언

     컴포넌트내 선언

      const [todoList, setTodoList] = useState<Todo[]>(INITIAL_TODOS);
      const [input, setInput] = useState<string>('');
      const [sort, setSort] = useState<SortType>('Ascending');
      const [filter, setFilter] = useState<string>('');

     

     일정 추가

      /**
       * 할일을 추가하는 함수
       */
      const addTodo = () => {
        const today = new Date();
        setTodoList([
          ...todoList,
          { id: uuidv4(), text: input, status: '미시작', date: today },
        ]);
        setInput('');
      };

     

     일정 제거

      /**
       * 할일을 제거하는 함수
       * @param id
       */
      const deleteTodo = (id: number) => {
        setTodoList((prevTodo) => prevTodo.filter((todo) => todo.id !== id));
      };

     

     일정의 상태를 완료로 만들기

      /**
       * 할일을 완료로 만드는 함수
       * @param id
       */
      const markTodoAsCompleted = (id: number) => {
        setTodoList((prevTodolist) =>
          prevTodolist.map((todo) =>
            todo.id === id ? { ...todo, status: '완료' } : todo,
          ),
        );
      };

     

     일정의 상태를 변경

      /**
       * 할일을 status에 맞게 변경하는 함수
       * @param id
       * @param status
       */
      const changeTodoStatus = (id: number, status: StatusType) => {
        setTodoList((prevTodolist) =>
          prevTodolist.map((todo) => (todo.id === id ? { ...todo, status } : todo)),
        );
      };

     

     일정을 정렬

      /**
       * 할일을 정렬하는 함수
       */
      const toggleSortOrder = () => {
        setSort((prevSort) =>
          prevSort === 'Ascending' ? 'Descending' : 'Ascending',
        );
      };

     

     각 Element의 Event들

      const handleChangeInput = (e: ChangeEvent<HTMLInputElement>) => {
        setInput(e.target.value);
      };
    
      const handleChangeFilter = (e: ChangeEvent<HTMLInputElement>) => {
        setFilter(e.target.value);
      };
    
      const handleClickAdd = () => {
        addTodo();
      };
    
      const handleClickDelete = (id: number) => {
        deleteTodo(id);
      };
    
      const handleClickComplete = (id: number) => {
        markTodoAsCompleted(id);
      };
    
      const handleClickSort = () => {
        toggleSortOrder();
      };
    
      const handleChangeStatus = (id: number, status: StatusType) => {
        changeTodoStatus(id, status);
      };

     

    코드 전체

    'use client';
    
    import { useState, ChangeEvent } from 'react';
    import { v4 as uuidv4 } from 'uuid';
    import StyledTodoList from './TodoList.styles';
    
    type StatusType = '미시작' | '진행중' | '완료';
    
    type SortType = 'Ascending' | 'Descending';
    
    interface Todo {
      id: number;
      text: string;
      status: StatusType;
      date: Date;
    }
    
    const INITIAL_TODOS: Todo[] = [];
    const SORT_TEXT = {
      Ascending: '오름차순',
      Descending: '내림차순',
    };
    
    export default function TodoList() {
      const [todoList, setTodoList] = useState<Todo[]>(INITIAL_TODOS);
      const [input, setInput] = useState<string>('');
      const [sort, setSort] = useState<SortType>('Ascending');
      const [filter, setFilter] = useState<string>('');
    
      /**
       * 할일을 추가하는 함수
       */
      const addTodo = () => {
        const today = new Date();
        setTodoList([
          ...todoList,
          { id: uuidv4(), text: input, status: '미시작', date: today },
        ]);
        setInput('');
      };
    
      /**
       * 할일을 제거하는 함수
       * @param id
       */
      const deleteTodo = (id: number) => {
        setTodoList((prevTodo) => prevTodo.filter((todo) => todo.id !== id));
      };
    
      /**
       * 할일을 완료로 만드는 함수
       * @param id
       */
      const markTodoAsCompleted = (id: number) => {
        setTodoList((prevTodolist) =>
          prevTodolist.map((todo) =>
            todo.id === id ? { ...todo, status: '완료' } : todo,
          ),
        );
      };
    
      /**
       * 할일을 status에 맞게 변경하는 함수
       * @param id
       * @param status
       */
      const changeTodoStatus = (id: number, status: StatusType) => {
        setTodoList((prevTodolist) =>
          prevTodolist.map((todo) => (todo.id === id ? { ...todo, status } : todo)),
        );
      };
    
      /**
       * 할일을 정렬하는 함수
       */
      const toggleSortOrder = () => {
        setSort((prevSort) =>
          prevSort === 'Ascending' ? 'Descending' : 'Ascending',
        );
      };
    
      const handleChangeInput = (e: ChangeEvent<HTMLInputElement>) => {
        setInput(e.target.value);
      };
    
      const handleChangeFilter = (e: ChangeEvent<HTMLInputElement>) => {
        setFilter(e.target.value);
      };
    
      const handleClickAdd = () => {
        addTodo();
      };
    
      const handleClickDelete = (id: number) => {
        deleteTodo(id);
      };
    
      const handleClickComplete = (id: number) => {
        markTodoAsCompleted(id);
      };
    
      const handleClickSort = () => {
        toggleSortOrder();
      };
    
      const handleChangeStatus = (id: number, status: StatusType) => {
        changeTodoStatus(id, status);
      };
    
      return (
        <StyledTodoList>
          <div className="TodoList__wrapper">
            <h1>Todo List</h1>
            <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
              <div style={{ display: 'flex', gap: '10px' }}>
                <p>작업 추가 영역 : </p>
                <input
                  value={input}
                  onChange={handleChangeInput}
                  placeholder="입력 필드"
                />
                <button type="button" onClick={handleClickAdd}>
                  작업 추가
                </button>
              </div>
    
              <div style={{ display: 'flex', gap: '10px' }}>
                <p>검색 영역: </p>
                <input
                  value={filter}
                  onChange={handleChangeFilter}
                  placeholder="검색 항목을 넣어주세요"
                />
              </div>
    
              <div style={{ display: 'flex', gap: '10px' }}>
                <p>정렬 상태 :</p>
                <button type="button" onClick={handleClickSort}>
                  {SORT_TEXT[sort]}
                </button>
              </div>
    
              <div>----------------------</div>
              {todoList &&
                todoList
                  .filter((todo) => todo.text.includes(filter))
                  .sort((a, b) => {
                    if (sort === 'Ascending') {
                      return a.text.localeCompare(b.text);
                    }
                    return b.text.localeCompare(a.text);
                  })
                  .map((list, index) => {
                    const { id, text, status, date } = list;
                    return (
                      <div
                        key={`${index + Math.random()}`}
                        style={{ display: 'flex', gap: '20px' }}
                        className={list.status === '완료' ? 'TodoList__cancel' : ''}
                      >
                        <p>{text}</p>
                        <select
                          onChange={(e) =>
                            handleChangeStatus(id, e.target.value as StatusType)
                          }
                          value={status}
                        >
                          <option value="미완성">미시작</option>
                          <option value="진행중">진행중</option>
                          <option value="완료">완료</option>
                        </select>
                        <button
                          type="button"
                          onClick={() => {
                            handleClickComplete(id);
                          }}
                        >
                          완료
                        </button>
                        <p>{date.toUTCString()}</p>
                        <button type="button" onClick={() => handleClickDelete(id)}>
                          제거
                        </button>
                      </div>
                    );
                  })}
            </div>
          </div>
        </StyledTodoList>
      );
    }

     

     

     실행 모습