Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions README2.md
Original file line number Diff line number Diff line change
@@ -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으로 나누기 오류를 방지하는 조건문을 추가합니다.
7 changes: 5 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import "./App.css";
import BasicTodo from "./step1/BasicTodo";
import AdvancedTodo from "./step2/AdvancedTodo";

function App() {
return <BasicTodo />;
return (
// <BasicTodo />
<AdvancedTodo />
);
}

export default App;
65 changes: 55 additions & 10 deletions src/step1/BasicTodo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="todo-container">
Expand All @@ -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)}
/>
<button className="add-button" onClick={addTodo}>
추가
Expand All @@ -55,8 +100,8 @@ const BasicTodo = () => {
key={index}
todo={todo}
index={index}
toggleTodoState={toggleTodoState}
deleteTodo={deleteTodo}
onToggle={toggleTodoState}
onDelete={deleteTodo}
/>
))}
</div>
Expand Down
12 changes: 9 additions & 3 deletions src/step1/TodoItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,28 @@ const TodoItem = ({ todo, index, onToggle, onDelete }) => {
className="todo-checkbox"
type="checkbox"
checked={todo.state === "완료"}
onChange={() => { }}
onChange={() => onToggle(index)}
/>

<div className="todo-content">
<p className={`todo-text ${todo.state === "완료" ? "completed" : ""}`}>
{/* TODO: 3. 할 일 내용 표시 */}
{todo.job}
</p>
</div>

<div className="todo-actions">
<span className={`todo-badge ${todo.state === "완료" ? "completed" : ""}`}>
<span
className={`todo-badge ${todo.state === "완료" ? "completed" : ""}`}
>
{/* TODO: 5. 삼항 연산자를 이용하여 상태 표시 배지 */}
{todo.state === "완료" ? "완료" : "미완료"}
</span>

{/* TODO: 6. 삭제 버튼 구현 onClick 함수를 완성해 봅시다.*/}
<button className="delete-button" onClick={() => { }}>×</button>
<button className="delete-button" onClick={() => onDelete(index)}>
×
</button>
</div>
</div>
);
Expand Down
116 changes: 116 additions & 0 deletions src/step2/AdvancedTodo.css
Original file line number Diff line number Diff line change
@@ -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;
}
Loading