diff --git a/README2.md b/README2.md new file mode 100644 index 0000000..5385438 --- /dev/null +++ b/README2.md @@ -0,0 +1,128 @@ +# 고급 투두리스트 (2단계) + +React의 복잡한 상태 관리와 필터 기능들을 구현하는 실습 프로젝트입니다. + +## AdvancedTodo.jsx 구현하기 + +``` +AdvancedTodo.jsx 고급 투두리스트의 메인 컴포넌트입니다. +TodoFilter.jsx 필터링 기능을 제공하는 컴포넌트입니다. +TodoActions.jsx 일괄 작업 기능을 제공하는 컴포넌트입니다. +``` + +### 1. 사용자 입력값, 필터 상태, 수정 관련 state들 생성 + +기존 기본 투두리스트에서 확장된 상태들을 추가합니다. +현재 필터 상태를 저장할 state와 수정 모드 관리를 위한 state들을 추가합니다. +이러한 상태들을 통해 더 복잡한 사용자 인터랙션을 처리할 수 있습니다. + +### 2. 새로운 할 일을 목록에 추가하는 함수 구현 + +기존의 배열 인덱스 대신 고유한 id를 사용하여 할 일을 관리합니다. +Date.now()를 활용하여 고유한 id를 생성하고, 새로운 할 일 객체를 생성합니다. +스프레드 연산자로 기존 배열을 복사한 후 새로운 할 일을 추가합니다. + +### 3. 할 일의 완료/미완료 상태를 전환하는 함수 구현 + +map 메서드를 사용하여 특정 id의 할 일 상태를 변경합니다. +조건부로 해당 id와 일치하는 할 일의 state 속성을 토글합니다. +불변성을 유지하면서 새로운 배열을 반환합니다. + +### 4. 특정 할 일을 목록에서 제거하는 함수 구현 + +filter 메서드를 사용하여 특정 id의 할 일을 제외한 새로운 배열을 생성합니다. +인덱스 기반이 아닌 id 기반으로 삭제하여 더 안전한 데이터 관리가 가능합니다. + +### 5. 할 일 텍스트 수정을 시작하는 함수 구현 + +수정 시작 함수는 수정 모드를 시작하며, 수정 중인 할 일의 ID와 텍스트 상태를 설정합니다. +사용자가 더블클릭하거나 수정 버튼을 클릭했을 때 호출됩니다. + +### 6. 수정된 할 일 텍스트를 저장하는 함수 구현 + +수정 저장 함수는 수정된 내용을 저장하고, map을 통해 해당 id의 내용을 업데이트합니다. +유효성 검사를 통해 빈 텍스트는 저장되지 않도록 처리합니다. + +### 7. 할 일 수정을 취소하고 원래 상태로 돌리는 함수 구현 + +수정 취소 함수는 수정을 취소하고 수정 관련 상태들을 초기화합니다. +ESC 키를 누르거나 취소 버튼을 클릭했을 때 호출됩니다. + +### 8. 엔터키를 눌렀을 때 할 일을 추가하는 키보드 이벤트 함수 구현 + +Enter 키로 할 일을 추가하는 기능을 제공합니다. +키보드 이벤트를 통해 더 나은 사용자 경험을 구현할 수 있습니다. + +### 9. 입력 필드와 사용자 입력값을 연결 + +기본 HTML input 태그를 사용하여 사용자 입력을 받습니다. +value, onChange, onKeyDown 속성을 적절히 연결하여 제어된 컴포넌트를 만듭니다. +placeholder를 통해 사용자에게 입력 가이드를 제공합니다. + +### 10. 필터링 기능을 위한 TodoFilter 컴포넌트 연결 + +TodoFilter 컴포넌트에 현재 필터 상태와 변경 함수를 props로 전달합니다. +컴포넌트 분리를 통해 재사용성과 유지보수성을 향상시킵니다. + +### 11. 전체 선택/삭제 기능을 위한 TodoActions 컴포넌트 연결 + +TodoActions 컴포넌트에 필요한 데이터와 함수들을 props로 전달합니다. +전체 선택/해제, 완료 항목 삭제 등의 일괄 작업 기능을 제공합니다. + +### 12. 필터링된 할 일 목록을 화면에 표시 + +필터링된 할 일 목록을 map으로 순회하여 각 할 일을 렌더링합니다. +빈 상태일 때 적절한 메시지를 표시하여 사용자 경험을 개선합니다. + +### 13. 할 일 완료 상태를 체크박스에 연결 + +체크박스의 checked 속성을 할 일의 완료 상태와 연결합니다. +onChange 이벤트를 통해 체크박스 클릭 시 상태를 토글합니다. + +### 14. 할 일 내용을 표시하고 더블클릭으로 수정 모드 전환 + +현재 수정 중인 할 일의 ID와 비교하여 수정 모드와 일반 모드를 조건부로 렌더링합니다. +수정 중일 때는 input 태그를, 일반 상태일 때는 텍스트로 표시합니다. +더블클릭 이벤트를 통해 수정 모드로 전환할 수 있습니다. + +### 15. 할 일의 상태 표시와 수정/삭제 버튼 구현 + +할 일의 완료/진행중 상태를 배지로 표시합니다. +수정 모드일 때는 저장/취소 버튼을, 일반 모드일 때는 수정/삭제 버튼을 표시합니다. +조건부 렌더링을 통해 적절한 버튼들을 보여줍니다. + +## TodoFilter.jsx 구현하기 + +### 1. 필터 옵션들 정의 + +필터 버튼에 사용할 옵션들을 배열로 정의합니다. +각 옵션은 key, label, color 속성을 가지며 동적 UI 생성에 활용됩니다. + +### 2. filters 배열을 map으로 렌더링 + +map 메서드를 사용하여 필터 옵션들을 동적으로 버튼으로 렌더링합니다. +key 속성을 반드시 지정하여 React의 효율적인 렌더링을 돕습니다. +onClick 이벤트에서 화살표 함수를 사용하여 매개변수를 전달합니다. + +## TodoActions.jsx 구현하기 + +### 1. 통계 계산 + +filter 메서드와 length 속성을 사용하여 완료된 할 일의 개수를 계산합니다. +totalCount는 전체 할 일의 개수를 나타내며 버튼의 활성화 상태를 결정합니다. + +### 2. 전체 선택/해제 버튼 + +allSelected 상태에 따라 버튼의 텍스트와 동작을 변경합니다. +disabled 속성을 사용하여 할 일이 없을 때 버튼을 비활성화합니다. +삼항 연산자를 활용하여 조건부 스타일링을 적용합니다. + +### 3. 완료 항목 삭제 버튼 + +completedCount가 0일 때 버튼을 비활성화하여 불필요한 동작을 방지합니다. +버튼 텍스트에 현재 완료된 개수를 표시하여 사용자에게 정보를 제공합니다. + +### 4. 진행률 표시 + +Math.round를 사용하여 완료율을 백분율로 계산하고 정수로 표시합니다. +totalCount가 0일 때 0으로 나누기 오류를 방지하는 조건문을 추가합니다. diff --git a/src/App.jsx b/src/App.jsx index d59aaa8..cf61de9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,11 @@ import "./App.css"; -import BasicTodo from "./step1/BasicTodo"; +import AdvancedTodo from "./step2/AdvancedTodo"; function App() { - return ; + return ( + // + + ); } export default App; diff --git a/src/step1/BasicTodo.jsx b/src/step1/BasicTodo.jsx index 5af15b9..e62c141 100644 --- a/src/step1/BasicTodo.jsx +++ b/src/step1/BasicTodo.jsx @@ -8,19 +8,59 @@ const BasicTodo = () => { { job: "장보기", state: "완료" }, { job: "산책하기", state: "미완료" }, ]); - // TODO: 1. 할 일 입력을 위한 state 생성 - + // TODO: 1. 할 일 입력을 위한 state 생성 => useState( // 초기값 ) + const [newTodo, setNewTodo] = useState(""); // state, setState + const [isComposing, setIsComposing] = useState(false); // TODO: 2. 할 일 추가 함수 구현 - const addTodo = () => { }; + // 불변성 지키기 => 원본 배열을 그대로 수정하지 않기 + const addTodo = () => { + // if(newTodo === "") return; // 공백 입력 방지 + // if(!newTodo) return; falsy 값 => 빈문자열 처리 주로 이렇게 사용 + if (newTodo.trim() === "") return; // early return + const updatedTodoList = [ + ...todoList, + { + job: newTodo, + state: "미완료", + }, + ]; + setTodoList(updatedTodoList); + setNewTodo("") + //setTodoList([...todoList, { job: newTodo, state: "미완료" }]); + }; // TODO: 3. 완료/미완료 토글 함수 구현 - const toggleTodoState = (index) => { }; + const toggleTodoState = (index) => { + // const toggledTodoList = [...todoList]; + // 해당 index를 가진 배열의 객체 요소의 staste 값을 반대로 토글링! + // toggledTodoList[index].state = toggledTodoList[index].state === "완료" ? "미완료" : "완료"; + const toggledTodoList = todoList.map((todo, i) => + i === index + ? { ...todo, state: todo.state === "완료" ? "미완료" : "완료" } + : todo + ); + //map => 새로운 배열 반환 좀 더 깔끔 할 수 있다. + setTodoList(toggledTodoList); + }; // TODO: 4. 할 일 삭제 함수 구현 - const deleteTodo = (index) => { }; + const deleteTodo = (index) => { + const deletedTodoList = todoList.filter((_, i) => i !== index); + // item, index => _ , i => _ 언더바는 사용하지 않는다는 의미 + // map, fileter => map((item, index) => {}) 객체와 인덱스 + setTodoList(deletedTodoList); + }; // TODO: 5. Enter 키 처리 함수 (한국어 입력 고려) - const handleKeyDown = (e) => { }; + const handleKeyDown = (e) => { // event를 받음. keyboradEvent + //console.log(e.key,e.nativeEvent.isComposing); + //if (e.nativeEvent.isComposing) return; // 조합 중이면 무시 + if (e.key === "Enter" && !isComposing) { + addTodo(); + } + // if(e.key !== "Enter") return; + // addTodo(); + }; return (
@@ -32,9 +72,14 @@ const BasicTodo = () => { type="text" placeholder="새로운 할 일을 입력하세요..." /* TODO: 7. input 값 채우기 */ - // value={} // 1번의 state 적용 - onChange={(e) => { }} // 1번의 setState 적용 + value={newTodo} + onChange={(e) => { + setNewTodo(e.target.value); + //지금 내가 발생시키고 있는 요소의 값을 가져와 변경 + }} // 1번의 setState 적용 onKeyDown={handleKeyDown} + onCompositionStart={() => setIsComposing(true)} + onCompositionEnd={() => setIsComposing(false)} />
diff --git a/src/step1/TodoItem.jsx b/src/step1/TodoItem.jsx index bbad841..44c1d70 100644 --- a/src/step1/TodoItem.jsx +++ b/src/step1/TodoItem.jsx @@ -9,22 +9,28 @@ const TodoItem = ({ todo, index, onToggle, onDelete }) => { className="todo-checkbox" type="checkbox" checked={todo.state === "완료"} - onChange={() => { }} + onChange={() => onToggle(index)} />

{/* TODO: 3. 할 일 내용 표시 */} + {todo.job}

- + {/* TODO: 5. 삼항 연산자를 이용하여 상태 표시 배지 */} + {todo.state === "완료" ? "완료" : "미완료"} {/* TODO: 6. 삭제 버튼 구현 onClick 함수를 완성해 봅시다.*/} - +
); diff --git a/src/step2/AdvancedTodo.css b/src/step2/AdvancedTodo.css new file mode 100644 index 0000000..2ae7100 --- /dev/null +++ b/src/step2/AdvancedTodo.css @@ -0,0 +1,116 @@ +.advanced-todo-item { + display: flex; + align-items: center; + padding: 16px; + background-color: white; + border-radius: 8px; + border: 1px solid #e5e7eb; + transition: all 0.2s ease; + position: relative; +} + +.advanced-todo-item:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.completed-indicator { + position: absolute; + top: 0; + left: 0; + width: 3px; + height: 100%; + background-color: #18181b; + border-radius: 2px 0 0 2px; +} + +.todo-checkbox { + width: 16px; + height: 16px; + margin-right: 12px; + cursor: pointer; + accent-color: #18181b; +} + +.todo-content { + flex: 1; + min-width: 0; +} + +.todo-text { + margin: 0; + font-size: 16px; + font-weight: 500; + word-break: break-word; +} + +.todo-text.completed { + text-decoration: line-through; + color: #9ca3af; +} + +.todo-text:not(.completed) { + text-decoration: none; + color: #1f2937; +} + +.todo-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.todo-badge { + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + border: 1px solid #e4e4e7; +} + +.todo-badge.completed { + background-color: #f4f4f5; + color: #18181b; +} + +.todo-badge:not(.completed) { + background-color: #fafafa; + color: #71717a; +} + +.edit-input { + flex: 1; + height: 32px; + padding: 0 8px; + border: 1px solid #18181b; + border-radius: 4px; + font-size: 14px; + outline: none; + background-color: white; + color: #1f2937; + caret-color: #18181b; +} + +.edit-actions { + display: flex; + gap: 4px; +} + +.edit-button { + width: 28px; + height: 28px; + border-radius: 4px; + border: 1px solid #e4e4e7; + background-color: white; + color: #71717a; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + outline: none; +} + +.edit-button:hover { + background-color: #f4f4f5; + color: #18181b; +} diff --git a/src/step2/AdvancedTodo.jsx b/src/step2/AdvancedTodo.jsx new file mode 100644 index 0000000..e94f801 --- /dev/null +++ b/src/step2/AdvancedTodo.jsx @@ -0,0 +1,267 @@ +import { useState } from "react"; +import "../components/shared/styles.css"; +import "./AdvancedTodo.css"; +import TodoFilter from "./TodoFilter"; +import TodoActions from "./TodoActions"; + +function AdvancedTodo() { + const [todoList, setTodoList] = useState([ + { id: 1, job: "장보기", state: "완료" }, + { id: 2, job: "산책하기", state: "미완료" }, + { id: 3, job: "책 읽기", state: "미완료" }, + ]); + // TODO: 1. 사용자 입력값을 저장할 state, 현재 필터 상태를 저장할 state, 수정 중인 할 일의 ID와 수정 중인 텍스트를 저장할 state들 생성 + const [inputValue, setInputValue] = useState(""); + const [filter, setFilter] = useState("all"); + const [editId, setEditId] = useState(null); + const [editText, setEditText] = useState(""); + const [isComposing, setIsComposing] = useState(false); + + // TODO: 2. 새로운 할 일을 목록에 추가하는 함수 구현 + const addTodo = () => { + if (inputValue.trim() === "") return; + const newTodo = { + id: Date.now(), + job: inputValue, + state: "미완료", + }; + setTodoList([...todoList, newTodo]); + setInputValue(""); + }; + + // TODO: 3. 할 일의 완료/미완료 상태를 전환하는 함수 구현 (특정 id를 받아서 처리) + const toggleTodoState = (id) => { + const toggledTodoList = todoList.map((todo) => + todo.id === id + ? { ...todo, state: todo.state === "완료" ? "미완료" : "완료" } + : todo + ); + setTodoList(toggledTodoList); + }; + + // TODO: 4. 특정 할 일을 목록에서 제거하는 함수 구현 (특정 id를 받아서 삭제) + const deleteTodo = (id) => { + const deletedTodoList = todoList.filter((todo) => todo.id !== id); + setTodoList(deletedTodoList); + }; + + // TODO: 5. 할 일 텍스트 수정을 시작하는 함수 구현 + const startEdit = (id, currentText) => { + setEditId(id); + setEditText(currentText); + setIsComposing(false); + }; + + // TODO: 6. 수정된 할 일 텍스트를 저장하는 함수 구현 + const saveEdit = (id) => { + if (editText.trim() === "") return; + const updatedTodoList = todoList.map((todo) => + todo.id === id ? { ...todo, job: editText } : todo + ); + setTodoList(updatedTodoList); + setEditId(null); + setEditText(""); + setIsComposing(false); + }; + + // TODO: 7. 할 일 수정을 취소하고 원래 상태로 돌리는 함수 구현 + const cancelEdit = () => { + setEditId(null); + setEditText(""); + setIsComposing(false); + }; + + // TODO: 8. 엔터키를 눌렀을 때 할 일을 추가하는 키보드 이벤트 함수 구현 + const handleKeyPress = (e) => { + if (e.key === "Enter" && !isComposing) { + if (editId !== null) { + saveEdit(editId); + } else { + addTodo(); + } + } + }; + + //전체 선택 함수 + const selectAll = () => { + const allSelect = todoList.map((todo) => ({ ...todo, state: "완료" })); + setTodoList(allSelect); + } + + //전체 해제 함수 + const deselectAll = () => { + const allDeselect = todoList.map((todo) => ({ ...todo, state: "미완료" })); + setTodoList(allDeselect); + } + + //완료된 할 일 일괄 삭제 함수 + const deleteCompleted = () => { + const deleteCompleteTodos = todoList.filter((todo) => todo.state !== "완료"); + setTodoList(deleteCompleteTodos); + } + + //필터링 함수 - switch문 사용 + const getFilteredTodoList = () => { + switch (filter) { + case "all": + return todoList; + case "completed": + return todoList.filter((todo) => todo.state === "완료"); + case "incomplete": + return todoList.filter((todo) => todo.state === "미완료"); + default: + return todoList; + } + } + + return ( +
+
+ {/* TODO: 9. 입력 필드와 사용자 입력값을 연결 */} +
+ { + setInputValue(e.target.value); + }} // 입력값이 변경될 때마다 상태 업데이트 + onKeyDown={handleKeyPress} + /> + +
+ + {/* TODO: 10. 필터링 기능을 위한 TodoFilter 컴포넌트 import*/} +
+ setFilter(newFilter)} + todoList={todoList} + /> +
+ {/* TODO: 11. 전체 선택/삭제 기능을 위한 TodoActions 컴포넌트 import */} +
+ todo.state === "완료") && + todoList.length > 0 + } + onSelectAll={selectAll} + onDeselectAll={deselectAll} + onDeleteCompleted={deleteCompleted} + todoList={todoList} + /> +
+ {/* TODO: 12. 필터링된 할 일 목록을 화면에 표시 */} +
+
+ {getFilteredTodoList().map((todo) => ( +
+ {/* TODO: 13. 할 일 완료 상태를 체크박스에 연결 */} + toggleTodoState(todo.id)} + /> + +
+ {editId === todo.id ? ( + setEditText(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !isComposing) { + saveEdit(todo.id); + } else if (e.key === "Escape") { + cancelEdit(); + } + }} + /> + ) : ( +

startEdit(todo.id, todo.job) + : undefined + } + style={{ cursor: "pointer" }} + > + {todo.job} +

+ )} +
+ +
+ + {todo.state === "완료" ? "완료" : "진행중"} + + +
+ {editId === todo.id ? ( + + ) : ( + + )} + +
+
+
+ ))} +
+
+
+
+ ); +} + +export default AdvancedTodo; diff --git a/src/step2/TodoActions.css b/src/step2/TodoActions.css new file mode 100644 index 0000000..16ed4dc --- /dev/null +++ b/src/step2/TodoActions.css @@ -0,0 +1,35 @@ +/* Todo Actions Styles */ +.actions-container { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 16px; + padding: 16px; + background-color: #fafafa; + border-radius: 6px; + border: 1px solid #e4e4e7; +} + +.actions-button { + padding: 6px 12px; + border: 1px solid #e4e4e7; + border-radius: 6px; + font-weight: 500; + font-size: 12px; + transition: colors 0.15s ease-in-out; + background-color: white; + color: #71717a; + outline: none; + cursor: pointer; +} + +.actions-button:disabled { + cursor: not-allowed; + background-color: #f4f4f5; + color: #a1a1aa; +} + +.actions-button:not(:disabled):hover { + background-color: #f4f4f5; + color: #18181b; +} \ No newline at end of file diff --git a/src/step2/TodoActions.jsx b/src/step2/TodoActions.jsx new file mode 100644 index 0000000..5c1fa8e --- /dev/null +++ b/src/step2/TodoActions.jsx @@ -0,0 +1,96 @@ +import './TodoActions.css'; + +function TodoActions({ + todoList, + onSelectAll, + onDeselectAll, + onDeleteCompleted, + allSelected +}) { + // TODO: 1. 통계 계산 + const totalCount = todoList.length; + const completedCount = todoList.filter((todo) => todo.state === "완료").length; + const progress = totalCount === 0 ? 0 : Math.round((completedCount / totalCount) * 100); + const deleteDisabled = completedCount === 0; + + return ( +
+ {/* TODO: 2. 전체 선택/해제 버튼 */} + + + {/* TODO: 3. 완료 항목 삭제 버튼 */} + + + {/* TODO: 4. 진행률 표시 */} +
+ {/* 진행률 표시 */} + 진행률: {progress}% +
+
+ ); +} + +export default TodoActions; + +/* +🎯 학습 포인트: +1. 복잡한 props 구조분해할당 +2. 계산된 값 (completedCount, totalCount) +3. 조건부 렌더링과 스타일링 +4. disabled 속성 활용 +5. Math.round()를 이용한 백분율 계산 + +💡 힌트: +- filter().length로 조건에 맞는 개수 계산 +- 삼항연산자로 조건부 텍스트와 스타일 적용 +- disabled 상태에서는 cursor와 opacity 조정 +- 0으로 나누기 방지를 위한 조건 체크 +*/ \ No newline at end of file diff --git a/src/step2/TodoFilter.jsx b/src/step2/TodoFilter.jsx new file mode 100644 index 0000000..0c0b24c --- /dev/null +++ b/src/step2/TodoFilter.jsx @@ -0,0 +1,47 @@ +import '../components/shared/styles.css'; + +function TodoFilter({ currentFilter, onFilterChange, todoList = [] }) { + // TODO: 1. 필터 옵션들 정의 + const filters = [ + { key: "all", label: "전체", color: "#007bff" }, + { key: "completed", label: "완료", color: "#28a745" }, + { key: "incomplete", label: "미완료", color: "#ffc107" }, + ]; + + // 각 필터별 개수 계산 함수 + const getCount = (key) => { + if (!Array.isArray(todoList)) return 0; + if (key === "all") return todoList.length; + if (key === "completed") + return todoList.filter((todo) => todo.state === "완료").length; + if (key === "incomplete") + return todoList.filter((todo) => todo.state === "미완료").length; + return 0; + }; + + return ( +
+ {/* TODO: 2. filters 배열을 map으로 렌더링 */} + {filters.map((filter) => ( + + ))} +
+ ); +} + +export default TodoFilter;