Search

To Do List - FrontEnd (React)

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
복사