Skip to content

Commit

Permalink
feat: #7 Todo 리스트 구현 (#13)
Browse files Browse the repository at this point in the history
# Description

Todo 페이지의 기능을 구현하였습니다.

@5unk3n 님과 함께 LiveShare로 페어 프로그래밍을 하였습니다!

## Changes
- TODO CRUD에 필요한 api를 작성하였습니다.
- [디렉토리 구조
예시](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/pull/3)에
맞추어
[src/apis/](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/tree/feat/%237-todo/src/apis)
경로에
[todo.ts](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/blob/feat/%237-todo/src/apis/todo.ts)를
작성하였습니다.
-
[src/apis/instance.ts](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/blob/main/src/apis/instance.ts)
의 AuthInstance를 사용하여 구현하였습니다.

-
[/todo](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/blob/feat/%237-todo/src/pages/Todo.tsx)
페이지에서 Todo를 추가, 조회, 수정, 삭제를 할 수 있도록 구현하였습니다.
-
[TodoList](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/blob/feat/%237-todo/src/components/TodoList/TodoList.tsx)와
[TodoItem](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/blob/feat/%237-todo/src/components/TodoItem/TodoItem.tsx)
컴포넌트를 작성하였습니다.

- TODO 조회


![조회](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/assets/93248349/d9b5cde7-cbb6-4b4b-809f-b1467e228efb)


```javascript
// src/components/TodoList/types.ts

export interface TodoType {
  id: number
  todo: string
  isCompleted: boolean
  userId: number
}
```

Todo의 type을 위와 같이 정의하였고,

```javascript
// src/apis/todo.ts

export const getTodosRequest = async (): Promise<TodoType[]> => {
  const { data } = await AuthInstance.get('/todos')
  return data
}
```

```javascript
// src/components/TodoList/TodoList.tsx

function TodoList() {
  const [todos, setTodos] = useState<TodoType[]>([])

  ...

  const getTodo = () => {
    getTodosRequest()
      .then((todos) => setTodos(todos))
      .catch((e: AxiosError) => alert(e.message))
  }

  useEffect(() => {
    getTodo()
  }, [])

  ...

}
```

```get``` 요청 성공시, 서버로부터 받아온 값을 todos에 ```useState<TodoType[]>([])```로 저장하였습니다.

```javascript
// src/components/TodoList/TodoList.tsx

<ul>
{todos.map((todo) => (
    <TodoItem
      key={todo.id}
      id={todo.id}
      todo={todo.todo}
      isCompleted={todo.isCompleted}
      updateTodo={updateTodo}
      deleteTodo={deleteTodo}
     />
  ))}
</ul>
```

또한, ```map()```을 사용하여 Todo 리스트의 내용을 렌더링 하였습니다.

- TODO 추가

![추가](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/assets/93248349/c19143e8-589f-472a-9cef-4219e4a8a957)

```javascript
// src/apis/todo.ts

export const createTodoRequest = async (todo: string): Promise<TodoType>
=> {
  const { data } = await AuthInstance.post('/todos', { todo })
  return data
}

```

```javascript
// src/components/TodoList/TodoList.tsx

function TodoList() {
  const [todos, setTodos] = useState<TodoType[]>([])
  const [newTodo, setNewTodo] = useState('')

  const createTodo = (todo: string) => {
    createTodoRequest(todo)
      .then((createdTodo) => {
        setTodos((prevTodos) => [...prevTodos, createdTodo])
        setNewTodo('')
      })
      .catch((e: AxiosError) => alert(e))
  }

  ...

}
```

```javascript
// src/components/TodoList/TodoList.tsx

<div>
  <label htmlFor="addTodo">
    <input
      id="addTodo"
      value={newTodo}
      onChange={(e) => setNewTodo(e.target.value)}
      data-testid="new-todo-input"
    />
  </label>
<button onClick={() => createTodo(newTodo)} type="button"
data-testid="new-todo-add-button">
    추가
  </button>
</div>

```

input의 ```onChange``` value를 newTodo에 ```useState('')```로 저장하였고, 추가 버튼을 누르면 서버로 ```post``` 요청을 보냅니다. 

```post``` 요청 성공시, 해당 값을
[Spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax)를
사용하여 todos에 추가 하도록 하였습니다.

- TODO 수정


![수정](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/assets/93248349/f7f15044-8171-4a1d-934b-65e17b23c690)

```ts
// src/components/TodoItem/TodoItem.tsx

function TodoItem({ id, todo, isCompleted, updateTodo, deleteTodo }: TodoItemProps) {

  ...

  const [todoModify, setTodoModify] = useState('')
  const [isEditMode, setIsEditMode] = useState(false)

  ...

  // 수정 버튼을 눌렀을 때
  const onEditButtonClicked = () => {
    setTodoModify(todo)
    setIsEditMode(true)
  }

  ...

  return (
    <li>
      <label>
        <input type="checkbox" checked={todoCheck} onChange={onTodoCheckChanged} />
        {isEditMode ? (
          <input
            data-testid="modify-input"
            type="text"
            onChange={onTodoModifyChanged}
            value={todoModify}
          />
        ) : (
          <span>{todo}</span>
        )}
      </label>
      {isEditMode ? (
        <>
          <button data-testid="submit-button" onClick={onSubmitButtonClicked}>
            제출
          </button>
          <button data-testid="cancel-button" onClick={onCancelButtonClicked}>
            취소
          </button>
        </>
      ) : (
        <>
          <button data-testid="modify-button" onClick={onEditButtonClicked}>
            수정
          </button>
          <button data-testid="delete-button" onClick={onDeleteButtonClicked}>
            삭제
          </button>
        </>
      )}
    </li>
  )
}
```

`isEditMode` 상태에 따라 렌더링 요소를 결정하고, 수정 버튼을 통해 토글합니다.

```ts
// src/components/TodoItem/TodoItem.tsx

// 제출 버튼을 눌렀을 때
const onSubmitButtonClicked = () => {
  updateTodo(id, todoModify, isCompleted)
  setIsEditMode(false)
}
```

제출 버튼 클릭 시 TodoList 컴포넌트에서 prop으로 받은 `updateTodo`함수를 실행합니다.

```ts
// src/components/TodoList/TodoList.tsx

const updateTodo = (id: number, todo: string, isCompleted: boolean) => {
  updateTodoRequest(id, todo, isCompleted)
    .then((updatedTodo) =>
      setTodos((prevTodos) =>
        prevTodos.map((prevTodo) => (prevTodo.id === id ? updatedTodo : prevTodo))
      )
    )
    .catch((e: AxiosError) => alert(e.message))
}
```

```ts
// src/apis/todo.ts

export const updateTodoRequest = async (
  id: number,
  todo: string,
  isCompleted: boolean
): Promise<TodoType> => {
  const { data } = await AuthInstance.put(`/todos/${id}`, { todo, isCompleted })
  return data
}
```

`put` 요청 성공시 todos에서 같은 id를 가진 todo를 변경하도록 했습니다.


- TODO 삭제


![삭제](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/assets/93248349/fb10f879-91c6-49d9-8ab0-83ba0b76f049)

```ts
// src/components/TodoItem/TodoItem.tsx

function TodoItem({ id, todo, isCompleted, updateTodo, deleteTodo }: TodoItemProps) {

  ...

  // 삭제 버튼을 눌렀을 때
  const onDeleteButtonClicked = () => {
    deleteTodo(id)
  }

  ...

  <button data-testid="delete-button" onClick={onDeleteButtonClicked}>
  삭제
  </button>

  ...

}
```

삭제 버튼 클릭 시 TodoList에서 prop으로 받은 `deleteTodo` 함수를 실행합니다.

```ts
// src/components/TodoList/TodoList.tsx

const deleteTodo = (id: number) => {
  deleteTodoRequest(id)
    .then(() => setTodos((prevTodos) => prevTodos.filter((prevTodo) => prevTodo.id !== id)))
    .catch((e: AxiosError) => alert(e.message))
}
```

```ts
// src/apis/todo.ts

export const deleteTodoRequest = async (id: number) => {
  return AuthInstance.delete(`/todos/${id}`)
}
```

`delete` 요청 성공 시 todos에서 `.filter()` 메서드를 사용해 같은 id를 가진 todo를 삭제하도록
했습니다.

- TODO 체크


![체크](https://github.com/wanted-pre-onboarding-team-12th-7/pre-onboarding-12th-1-7/assets/93248349/6a73374c-df0b-4713-9bc1-8544ce5be00f)

```ts
// src/components/TodoList/TodoList.tsx

function TodoItem({ id, todo, isCompleted, updateTodo, deleteTodo }: TodoItemProps) {
  const [todoCheck, setTodoCheck] = useState(isCompleted)

  ...

  // TODO의 체크박스가 변경 되었을 때
  const onTodoCheckChanged = () => {
    updateTodo(id, todo, !todoCheck)
    setTodoCheck(!todoCheck)
  }

  ...

  <label>
    <input type="checkbox" checked={todoCheck} onChange={onTodoCheckChanged} />
    {isEditMode ? (
      <input
        data-testid="modify-input"
        type="text"
        onChange={onTodoModifyChanged}
        value={todoModify}
      />
    ) : (
      <span>{todo}</span>
    )}
  </label>
}
```

todo의 완료 여부는 `todoCheck` 상태로 결정됩니다.

TODO 수정 기능과 같이 `updateTodo`함수를 사용하며 `todoCheck`를 not 연산자로 변경해 요청합니다.
  • Loading branch information
5unk3n authored Aug 24, 2023
2 parents 8559e65 + dfb599b commit 008af7b
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 0 deletions.
25 changes: 25 additions & 0 deletions src/apis/todo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { AuthInstance } from './instance'
import { TodoType } from '../components/TodoList/types'

export const createTodoRequest = async (todo: string): Promise<TodoType> => {
const { data } = await AuthInstance.post('/todos', { todo })
return data
}

export const getTodosRequest = async (): Promise<TodoType[]> => {
const { data } = await AuthInstance.get('/todos')
return data
}

export const updateTodoRequest = async (
id: number,
todo: string,
isCompleted: boolean
): Promise<TodoType> => {
const { data } = await AuthInstance.put(`/todos/${id}`, { todo, isCompleted })
return data
}

export const deleteTodoRequest = async (id: number) => {
return AuthInstance.delete(`/todos/${id}`)
}
81 changes: 81 additions & 0 deletions src/components/TodoItem/TodoItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useState } from 'react'
import TodoItemProps from './types'

function TodoItem({ id, todo, isCompleted, updateTodo, deleteTodo }: TodoItemProps) {
const [todoCheck, setTodoCheck] = useState(isCompleted)
const [todoModify, setTodoModify] = useState('')
const [isEditMode, setIsEditMode] = useState(false)

// TODO의 체크박스가 변경 되었을 때
const onTodoCheckChanged = () => {
updateTodo(id, todo, !todoCheck)
setTodoCheck(!todoCheck)
}

// TODO input창의 값이 변경 되었을 때 (수정모드)
const onTodoModifyChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const modifiedValue = e.target.value
setTodoModify(modifiedValue)
}

// 수정 버튼을 눌렀을 때
const onEditButtonClicked = () => {
setTodoModify(todo)
setIsEditMode(true)
}

// 취소 버튼을 눌렀을 때
const onCancelButtonClicked = () => {
setIsEditMode(false)
}

// 제출 버튼을 눌렀을 때
const onSubmitButtonClicked = () => {
updateTodo(id, todoModify, isCompleted)
setIsEditMode(false)
}

// 삭제 버튼을 눌렀을 때
const onDeleteButtonClicked = () => {
deleteTodo(id)
}

return (
<li>
<label>
<input type="checkbox" checked={todoCheck} onChange={onTodoCheckChanged} />
{isEditMode ? (
<input
data-testid="modify-input"
type="text"
onChange={onTodoModifyChanged}
value={todoModify}
/>
) : (
<span>{todo}</span>
)}
</label>
{isEditMode ? (
<>
<button data-testid="submit-button" onClick={onSubmitButtonClicked}>
제출
</button>
<button data-testid="cancel-button" onClick={onCancelButtonClicked}>
취소
</button>
</>
) : (
<>
<button data-testid="modify-button" onClick={onEditButtonClicked}>
수정
</button>
<button data-testid="delete-button" onClick={onDeleteButtonClicked}>
삭제
</button>
</>
)}
</li>
)
}

export default TodoItem
8 changes: 8 additions & 0 deletions src/components/TodoItem/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { TodoType } from '../TodoList/types'

interface TodoItemProps extends Omit<TodoType, 'userId'> {
updateTodo: (id: number, todo: string, isCompleted: boolean) => void
deleteTodo: (id: number) => void
}

export default TodoItemProps
83 changes: 83 additions & 0 deletions src/components/TodoList/TodoList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useState, useEffect } from 'react'

import TodoItem from '../TodoItem/TodoItem'
import { TodoType } from './types'
import {
createTodoRequest,
getTodosRequest,
updateTodoRequest,
deleteTodoRequest,
} from '../../apis/todo'
import { AxiosError } from 'axios'

function TodoList() {
const [todos, setTodos] = useState<TodoType[]>([])
const [newTodo, setNewTodo] = useState('')

const createTodo = (todo: string) => {
createTodoRequest(todo)
.then((createdTodo) => {
setTodos((prevTodos) => [...prevTodos, createdTodo])
setNewTodo('')
})
.catch((e: AxiosError) => alert(e))
}

const getTodo = () => {
getTodosRequest()
.then((todos) => setTodos(todos))
.catch((e: AxiosError) => alert(e.message))
}

const updateTodo = (id: number, todo: string, isCompleted: boolean) => {
updateTodoRequest(id, todo, isCompleted)
.then((updatedTodo) =>
setTodos((prevTodos) =>
prevTodos.map((prevTodo) => (prevTodo.id === id ? updatedTodo : prevTodo))
)
)
.catch((e: AxiosError) => alert(e.message))
}

const deleteTodo = (id: number) => {
deleteTodoRequest(id)
.then(() => setTodos((prevTodos) => prevTodos.filter((prevTodo) => prevTodo.id !== id)))
.catch((e: AxiosError) => alert(e.message))
}

useEffect(() => {
getTodo()
}, [])

return (
<div>
<div>
<label htmlFor="addTodo">
<input
id="addTodo"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
data-testid="new-todo-input"
/>
</label>
<button onClick={() => createTodo(newTodo)} type="button" data-testid="new-todo-add-button">
추가
</button>
</div>
<ul>
{todos.map((todo) => (
<TodoItem
key={todo.id}
id={todo.id}
todo={todo.todo}
isCompleted={todo.isCompleted}
updateTodo={updateTodo}
deleteTodo={deleteTodo}
/>
))}
</ul>
</div>
)
}

export default TodoList
6 changes: 6 additions & 0 deletions src/components/TodoList/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface TodoType {
id: number
todo: string
isCompleted: boolean
userId: number
}
2 changes: 2 additions & 0 deletions src/pages/Todo.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useNavigate } from 'react-router-dom'
import TodoList from '../components/TodoList/TodoList'

function Todo() {
const navigate = useNavigate()

return (
<div>
투두리스트 페이지입니다.
<TodoList />
<button
onClick={() => {
localStorage.removeItem('access_token')
Expand Down

0 comments on commit 008af7b

Please sign in to comment.