To Do List - Front (React)
“Todo List - 할 일 목록 UI 만들기”
작업 파일
•
App.js
•
App.css
•
components
◦
TodoContainer.jsx
◦
TodoHeader.jsx
◦
TodoInput.jsx
◦
TodoList.jsx
◦
TodoItem.jsx
◦
TodoFooter.jsx
컴포넌트 구조
•
App.
◦
TodoContainer
▪
TodoHeader
▪
TodoInput
▪
TodoList
•
TodoItem
▪
TodoFooter
코드 작업
•
components
◦
TodoContainer.jsx
◦
TodoHeader.jsx
◦
TodoInput.jsx
◦
TodoList.jsx
◦
TodoItem.jsx
◦
TodoFooter.jsx
•
App.css
•
App.js
TodoContainer.jsx
import React, { useEffect, useState } from 'react'
import TodoHeader from './TodoHeader'
import TodoInput from './TodoInput'
import TodoList from './TodoList'
import TodoFooter from './TodoFooter'
const TodoContainer = () => {
// ✅ state
const [todoList, setTodoList] = useState([]);
const [input, setInput] = useState('')
// ❓ hook
useEffect(() => {
// ✅ Mount, Update
// 할일 목록 요청 [GET]
fetch('http://192.168.30.119:8080/todos')
.then( ( response ) => response.json() )
.then( ( data ) => setTodoList(data) )
.catch( (error) => console.log(error) );
console.log('[GET] - /todos - 할일목록 요청');
// ✅ UnMount
return () => {
}
}, [])
// 할일 등록
const onSubmit = () => {
console.log('할일 : ' + input);
if( input == '' ) {
// alert('할 일을 입력해주세요')
// return
}
const data = {
name: input == '' ? '제목없음' : input, // 할일 제목
status: 0, // 완료 여부(미완료-0,완료-1)
};
const init = {
method : 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
};
// 할일 등록 [POST]
// ➡ 새로 등록된 할일 데이터
fetch('http://192.168.30.119:8080/todos',init)
.then( ( response ) => response.json() )
.then( ( data ) => setTodoList([data, ...todoList]) )
.catch( (error) => console.log(error) );
setInput('')
}
// 할일 입력 이벤트
const onChange = (e) => {
setInput(e.target.value);
}
// 할일 완료 여부 수정
const onToggle = (todo) => {
console.log('할일 완료 여부 처리');
console.log(todo);
// PUT 요청
const data = {
no: todo.no,
name: todo.name,
status: todo.status ? 0 : 1,
}
const init = {
method: 'PUT',
headers: {
'Content-Type' : 'application/json',
},
body: JSON.stringify(data),
}
// 할일 수정 [PUT]
// ➡ '수정 완료 메시지'
fetch('http://192.168.30.119:8080/todos',init)
.then( ( response ) => response.text() )
.then( ( data ) => console.log(data) )
.catch( (error) => console.log(error) );
// 할일 완료 처리 후,
// ➡ 클릭한 할일의 완료 여부를 toggle
// let updatedTodoList = todoList.map((item) => {
// return item.no === todo.no ? {...item, status: !item.status} : item;
// });
// })
// ➡ 완료된 할일은 아래쪽 정렬
// let sortedTodoList = updatedTodoList.sort((a, b) => {
// 정렬 기준
// 1. 완료 여부 - 0, 1 오름차순
// 2. 할일 번호 내림차순
// return a.status - b.status == 0 ? b.no - a.no : a.status - b.status;
// })
const sortedTodoList
= todoList.map((item) => item.no === todo.no ? {...item, status: !item.status} : item)
.sort((a, b) => a.status - b.status == 0 ? b.no - a.no : a.status - b.status)
setTodoList(sortedTodoList)
};
// 할일 삭제
const onDelete = (no) => {
// DELETE 요청
const init = {
method: 'DELETE',
};
// 할일 삭제 [DELETE]
// ➡ '삭제 완료 메시지'
fetch(`http://192.168.30.119:8080/todos/${no}`,init)
.then( ( response ) => response.text() )
.then( ( data ) => console.log(data) )
.catch( (error) => console.log(error) );
// 삭제된 할일 항목 제거
const updatedTodoList = todoList.filter( (todo) => todo.no != no )
setTodoList( updatedTodoList )
}
// 전체 삭제
const onDeleteAll = () => {
// [DELETE] /todos/-1
// DELETE 요청
const init = {
method: 'DELETE',
};
// 전체 할일 삭제 [DELETE]
// ➡ '삭제 완료 메시지'
fetch(`http://192.168.30.119:8080/todos/-1`,init)
.then( ( response ) => response.text() )
.then( ( data ) => console.log(data) )
.catch( (error) => console.log(error) );
// 삭제된 할일 항목 제거
setTodoList( [] )
}
// 전체 완료
const onCompleteAll = () => {
// PUT 요청
// 전체완료
// /todos : data : { no : -1 }
const data = {
no: -1,
}
const init = {
method: 'PUT',
headers: {
'Content-Type' : 'application/json',
},
body: JSON.stringify(data),
}
// 할일 전체 완료 [PUT]
// ➡ '수정 완료 메시지'
fetch('http://192.168.30.119:8080/todos',init)
.then( ( response ) => response.text() )
.then( ( data ) => console.log(data) )
.catch( (error) => console.log(error) );
const sortedTodoList
= todoList.map((item) => ({ ...item, status: 1 }) )
.sort((a, b) => a.status - b.status == 0 ? b.no - a.no : a.status - b.status)
setTodoList(sortedTodoList)
}
// 할일 수정
const onUpdate = (todo) => {
console.log('할일 수정 처리');
console.log(todo);
// PUT 요청
const data = {
no: todo.no,
name: todo.name,
status: todo.status,
}
const init = {
method: 'PUT',
headers: {
'Content-Type' : 'application/json',
},
body: JSON.stringify(data),
}
// 할일 수정 [PUT]
// ➡ '수정 완료 메시지'
fetch('http://192.168.30.119:8080/todos',init)
.then( ( response ) => response.text() )
.then( ( data ) => console.log(data) )
.catch( (error) => console.log(error) );
const sortedTodoList
= todoList.sort((a, b) => a.status - b.status == 0 ? b.no - a.no : a.status - b.status)
setTodoList(sortedTodoList)
};
return (
<div className="container">
<TodoHeader/>
<TodoInput input={input} onChange={onChange} onSubmit={onSubmit}/>
<TodoList todoList={todoList} onToggle={onToggle} onDelete={onDelete} onUpdate={onUpdate} />
<TodoFooter onDeleteAll={onDeleteAll} onCompleteAll={onCompleteAll} />
</div>
)
}
export default TodoContainer
JavaScript
복사
TodoHeader.jsx
import React from 'react'
const TodoHeader = () => {
return (
<div className='header'>
<h1>To Do List</h1>
</div>
)
}
export default TodoHeader
JavaScript
복사
TodoInput.jsx
import React, { useState } from 'react'
const TodoInput = ({ input, onSubmit, onChange }) => {
return (
<div>
<form className='form'>
<input placeholder='할 일 입력'
className='input'
onChange={onChange}
value={input}
/>
<button type='button'
className='btn'
onClick={onSubmit}
>추가</button>
</form>
</div>
)
}
export default TodoInput
JavaScript
복사
TodoList.jsx
import React from 'react'
import TodoItem from './TodoItem'
const TodoList = ( { todoList, onToggle, onDelete, onUpdate } ) => {
return (
<ul className="todoList">
{todoList.map( (todo) => (
<TodoItem
key={todo.no}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
onUpdate={onUpdate}
/>
))}
</ul>
)
}
export default TodoList
JavaScript
복사
TodoItem.jsx
import React from 'react'
const TodoItem = ( { todo, onToggle, onDelete, onUpdate } ) => {
let { no, name, status } = todo;
status = status == 1 ? true : false;
const className = status ? 'todoItem active' : 'todoItem';
return (
<li className={className}>
<div className="item">
<input type="checkbox"
id={todo.no}
checked={status}
onChange={ () => onToggle(todo) } />
<label htmlFor={todo.no}></label>
{/* 할일 */}
<input type="text"
id={`name-${todo.no}`}
className='input'
// value={todo.name}
defaultValue={todo.name}
/>
</div>
<div className="item">
<button className='btn btn-sm'
onClick={ () => {
todo.name = document.getElementById(`name-${todo.no}`).value;
onUpdate(todo);
}}>✅</button>
<button className='btn btn-sm' onClick={ () => onDelete(no) }>❌</button>
</div>
</li>
)
}
export default TodoItem
JavaScript
복사
TodoFooter.jsx
import React from 'react'
const TodoFooter = ({ onDeleteAll, onCompleteAll }) => {
return (
<div className='footer'>
<div className="item">
<button className='btn' onClick={ onDeleteAll }>전체삭제</button>
</div>
<div className="item">
<button className='btn' onClick={ onCompleteAll }>전체완료</button>
</div>
</div>
)
}
export default TodoFooter
JavaScript
복사
App.css
.container {
width: 480px;
min-height: 600px;
margin: 100px auto;
padding: 20px;
border: 2px solid cornflowerblue;
border-radius: 10px;
box-shadow: 5px 5px 5px rgba(0,0,0,0.3);
background-color: rgba(200,200,200,0.1);
}
.header {
text-align: center;
font-size: 20px;
color: royalblue;
}
.form {
display: flex;
justify-content: space-between;
margin: 10px 10px 10px 0;
}
.form .input {
width: 80%;
height: 30px;
}
.input::placeholder {
color: royalblue;
}
.input {
border: none;
border-bottom: 1px solid royalblue;
padding-left: 12px;
font-size: 18px;
outline: none;
color: blue;
font-weight: bold;
background-color: transparent;
}
.form .btn {
width: 15%;
}
.btn {
width: 80px;
height: 40px;
padding: 0 10px;
font-weight: bold;
border: none;
border-radius: 12px;
background-color: cornflowerblue;
color: white;
cursor: pointer;
}
.btn.btn-sm {
width: auto;
height: 40px;
padding: 0 12px;
}
.btn:hover {
background-color: royalblue;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
}
.btn:active {
box-shadow: none;
}
.todoList {
height: 500px;
overflow-y: auto;
}
ul { margin: 0; padding: 0; }
.todoItem {
list-style-type: none;
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px 10px 10px 0;
padding: 20px;
border: 2px solid cornflowerblue;
border-radius: 12px;
color: royalblue;
font-size: 18px;
box-shadow: 5px 5px 5px rgba(0,0,0,0.3);
}
.todoItem.active {
background-color: royalblue;
color: white;
}
.todoItem.active .btn:hover {
background-color: blue;
}
.todoItem .item {
display: flex;
align-items: center;
gap: 14px;
}
/* 체크박스 */
.todoItem input[type='checkbox'] {
display: none;
}
.todoItem label {
display: inline-block;
width: 20px;
height: 20px;
border: 3px dashed royalblue;
border-radius: 50%;
}
.todoItem input:checked ~ label {
background-color: cornflowerblue;
border: 3px solid blue;
}
.todoItem input:checked ~ input {
text-decoration: line-through;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px 10px 10px 0;
}
/* 스크롤바 */
::-webkit-scrollbar-track
{
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
background-color: transparent;
border-radius: 10px;
}
::-webkit-scrollbar
{
width: 10px;
background-color: transparent;
}
::-webkit-scrollbar-thumb
{
border-radius: 10px;
background-image: -webkit-gradient(linear,
left bottom,
left top,
color-stop(0.44, rgb(122,153,217)),
color-stop(0.72, rgb(73,125,189)),
color-stop(0.86, rgb(28,58,148)));
}
/* 배경 */
body {
background: linear-gradient(
-45deg,
#ffffff,
#ffe4e9,
#fcc8e2,
#f6baed,
#efb0ff,
#d3a2ff,
#8eb8ff,
#91efff,
#d5f9ff
);
background-size: 400% 400%;
animation: gradient 9s ease infinite;
height: 100vh;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
CSS
복사
App.js
import './App.css';
import TodoContainer from './components/TodoContainer';
function App() {
return (
<>
<TodoContainer/>
</>
);
}
export default App;
JavaScript
복사