-
Notifications
You must be signed in to change notification settings - Fork 8
[2주차] 이지수 미션 제출합니다. #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
52e64dd
7139238
5ebbb94
01078dc
c3eabb3
ebbd0d4
c0bb087
38ea302
78e6fb6
b9eb4fa
2d78e6e
60ada81
b4b5049
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,57 @@ | ||
| /** @jsxImportSource @emotion/react */ | ||
| import { useState, useEffect } from 'react'; | ||
| import { css } from '@emotion/react'; | ||
| import TodoInput from './components/TodoInput'; | ||
| import TodoList from './components/TodoList'; | ||
| import TodoHeader from './components/TodoHeader'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컴포넌트 이름도 직관적으로 지어 주셔서 아주 좋습니다!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저도 동의합니다 ! 항상 이름 짓는건 어려워요 😞 |
||
|
|
||
|
|
||
| const containerStyle = css` | ||
| max-width: 400px; | ||
| margin: 100px auto; | ||
| padding: 20px; | ||
| background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%); | ||
| border-radius: 20px; | ||
| box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); | ||
| `; | ||
|
|
||
|
|
||
| function App() { | ||
| return <div>React Todo</div>; | ||
| const [todos, setTodos] = useState(() => { | ||
| // 로컬 스토리지에서 저장된 todos 가져오기 | ||
| const savedTodos = localStorage.getItem('todos'); | ||
| return savedTodos ? JSON.parse(savedTodos) : []; // 저장된 값이 있으면 파싱하여 사용, 없으면 빈 배열 | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useEffect에서 함수를 사용할 경우를 대비하여 변수, 함수 선언 맨 마지막에 위치하도록 미리 습관을 들여보는 것은 어떨까요 ?! |
||
| // todos 상태가 변경될 때마다 로컬 스토리지에 저장 | ||
| localStorage.setItem('todos', JSON.stringify(todos)); | ||
| }, [todos]); | ||
|
|
||
| // 새 할 일을 추가하는 함수 | ||
| const addTodo = (text) => { | ||
| setTodos(todos.concat({ text, completed: false })); // 새로운 할 일을 기존 할 일 리스트에 추가 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. boolean 값을 저장하는 변수는 맞다, 아니다 2가지 상태만 나타내므로 더욱 더 네이밍에 신경써주는 것이 좋습니다! completed 보다 isCompleted 와 같은 의문문 형식의 이름을 더 많이 사용합니다 🤩
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저도 동의합니다 !! boolean 값을 갖는 변수명은� 보통 is를 접두어로 사용하죠 ~! |
||
| }; | ||
|
|
||
| // 특정 인덱스의 할 일을 삭제하는 함수 | ||
| const deleteTodo = (index) => { | ||
| setTodos(todos.filter((_, i) => i !== index)); // 해당 인덱스의 할 일을 제외하고 새로운 리스트 생성 | ||
| }; | ||
|
|
||
| // 특정 인덱스의 할 일의 완료 상태를 토글하는 함수 | ||
| const toggleTodo = (index) => { | ||
| setTodos(todos.map((todo, i) => | ||
| i === index ? { ...todo, completed: !todo.completed } : todo | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 특정 아이템을 토글/삭제 하려고 할 때, 다른 아이템이 토글/삭제 되는 경우가 있는데, 이 경우 배열 내 요소가 달라지면서 인덱스 값도 함께 바뀌게 되어서 발생하는 문제 같아요! |
||
| )); // 할 일의 완료 상태를 반전시킴 | ||
| }; | ||
|
|
||
| return ( | ||
| <div css={containerStyle}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. emotion css 사용 멋져요 👍 |
||
| <TodoHeader /> | ||
| <TodoInput addTodo={addTodo} /> | ||
| <TodoList todos={todos} deleteTodo={deleteTodo} toggleTodo={toggleTodo} /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default App; | ||
| export default App; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| /** @jsxImportSource @emotion/react */ | ||
| import { css } from '@emotion/react'; | ||
|
|
||
| const buttonStyle = css` | ||
| padding: 10px; | ||
| cursor: pointer; | ||
| background-color: #ff7f7f; | ||
| border: none; | ||
| border-radius: 5px; | ||
| color: white; | ||
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); | ||
| margin-left: 8px; | ||
| `; | ||
|
|
||
| function TodoButton({ onClick, children }) { | ||
| return ( | ||
| <button css={buttonStyle} onClick={onClick}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. onClick 함수 이름으로 동작이 어떤 것인지 예측 가능하게 작성해주시면 더 좋을 것 같습니다 🤩 |
||
| {children} {/* 버튼 내부의 내용 (아이콘이나 텍스트) */} | ||
| </button> | ||
| ); | ||
| } | ||
|
|
||
| export default TodoButton; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| /** @jsxImportSource @emotion/react */ | ||
| import { css } from '@emotion/react'; | ||
|
|
||
| const titleStyle = css` | ||
| font-size: 24px; | ||
| text-align: center; | ||
| margin-bottom: 20px; | ||
| `; | ||
|
|
||
| function TodoHeader() { | ||
| return <h1 css={titleStyle}>📚 TodoList</h1>; | ||
| } | ||
|
|
||
| export default TodoHeader; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| //** @jsxImportSource @emotion/react */ | ||
| import { useState } from 'react'; | ||
| import { css } from '@emotion/react'; | ||
|
|
||
| const inputContainerStyle = css` | ||
| margin-bottom: 20px; | ||
| display: flex; | ||
| justify-content: center; | ||
| align-items: center; | ||
| `; | ||
|
|
||
| const inputStyle = css` | ||
| padding: 10px; | ||
| width: 200px; | ||
| margin-right: 10px; | ||
| border: none; | ||
| border-radius: 5px; | ||
| outline: none; | ||
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); | ||
| `; | ||
|
|
||
| const buttonStyle = css` | ||
| padding: 10px; | ||
| cursor: pointer; | ||
| background-color: #ff7f7f; | ||
| border: none; | ||
| border-radius: 5px; | ||
| color: white; | ||
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); | ||
| `; | ||
|
|
||
| function TodoInput({ addTodo }) { | ||
| const [input, setInput] = useState(''); // 입력 값을 관리하기 위한 상태 선언 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 어떤 것을 입력하여 상태 관리할 것인지 알기 쉽도록 변수의 이름을 더 상세하게 적어주시면 좋을 것 같아요! ex. inputTodo |
||
|
|
||
| // 폼 제출 시 호출되는 함수 | ||
| const handleSubmit = (e) => { | ||
| e.preventDefault(); // 기본 폼 제출 동작을 막음 | ||
| if (!input.trim()) return; // 입력 값이 공백일 경우 아무 작업도 하지 않음 | ||
| addTodo(input); | ||
| setInput(''); | ||
| }; | ||
|
|
||
| return ( | ||
| <form css={inputContainerStyle} onSubmit={handleSubmit}> | ||
| <input | ||
| css={inputStyle} | ||
| type="text" | ||
| value={input} | ||
| onChange={(e) => setInput(e.target.value)} // 입력 값이 변경될 때마다 상태 업데이트 | ||
| placeholder="할일을 입력하세요" | ||
| /> | ||
| <button css={buttonStyle} type="submit">+</button> {/* 할 일을 추가하는 버튼 */} | ||
| </form> | ||
| ); | ||
| } | ||
|
|
||
| export default TodoInput; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,38 @@ | ||||||
| /** @jsxImportSource @emotion/react */ | ||||||
| import { css } from '@emotion/react'; | ||||||
| import TodoButton from './TodoButton'; | ||||||
|
|
||||||
| // Todo 항목의 기본 스타일 정의 | ||||||
| const itemStyle = css` | ||||||
| display: flex; | ||||||
| align-items: center; | ||||||
| justify-content: space-between; | ||||||
| padding: 10px 0; | ||||||
| border-bottom: 1px solid rgba(255, 255, 255, 0.5); | ||||||
| cursor: pointer; | ||||||
| color: #fff; | ||||||
| `; | ||||||
|
|
||||||
| // 완료된 Todo 항목 스타일 정의 | ||||||
| const completedStyle = css` | ||||||
| text-decoration: line-through; // 완료된 항목을 취소선으로 표시 | ||||||
| color: #aaa; | ||||||
| `; | ||||||
|
|
||||||
| // 개별 Todo 항목을 나타내는 컴포넌트 | ||||||
| function TodoItem({ todo, index, deleteTodo, toggleTodo }) { | ||||||
| return ( | ||||||
| <div css={[itemStyle, todo.completed ? completedStyle : null]}> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드와 같은 경우에서도 isCompleted 를 사용하면, "완료 시에는 completeStyle을 사용하고 아직 완료하지 않았다면 아무런 스타일을 적용하지 않는구나" 라고 명확하게 이해하기가 쉬워집니다!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 조건이 false일 때 null을 리턴하기보다는
Suggested change
|
||||||
| {/* 할 일 텍스트 표시 */} | ||||||
| <span>{todo.text}</span> | ||||||
| <div> | ||||||
| {/* 할 일 완료 상태를 토글하는 버튼 */} | ||||||
| <TodoButton onClick={() => toggleTodo(index)}>✔️</TodoButton> | ||||||
| {/* 할 일을 삭제하는 버튼 */} | ||||||
| <TodoButton onClick={() => deleteTodo(index)}>🗑️</TodoButton> | ||||||
| </div> | ||||||
| </div> | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| export default TodoItem; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /** @jsxImportSource @emotion/react */ | ||
| import { css } from '@emotion/react'; | ||
| import TodoListSection from './TodoListSection'; | ||
|
|
||
|
|
||
| const listContainerStyle = css` | ||
| width: 300px; | ||
| max-width: 100%; | ||
| margin: 0 auto; | ||
| `; | ||
|
|
||
| function TodoList({ todos, deleteTodo, toggleTodo }) { | ||
| const pendingTodos = todos.filter((todo) => !todo.completed); // 완료되지 않은 할 일만 필터링 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분도 isCompleted 마찬가지! |
||
| const completedTodos = todos.filter((todo) => todo.completed); // 완료된 할 일만 필터링 | ||
|
|
||
| return ( | ||
| <div css={listContainerStyle}> | ||
| {/* TO DO 섹션 */} | ||
| <TodoListSection | ||
| title="🗒️ TO DO" | ||
| todos={pendingTodos} | ||
| deleteTodo={deleteTodo} | ||
| toggleTodo={toggleTodo} | ||
| /> | ||
| {/* DONE 섹션 */} | ||
| <TodoListSection | ||
| title="💿 DONE" | ||
| todos={completedTodos} | ||
| deleteTodo={deleteTodo} | ||
| toggleTodo={toggleTodo} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default TodoList; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| /** @jsxImportSource @emotion/react */ | ||
| import { css } from '@emotion/react'; | ||
| import TodoItem from './TodoItem'; | ||
|
|
||
| const sectionStyle = css` | ||
| margin-bottom: 20px; | ||
| `; | ||
|
|
||
| function TodoListSection({ title, todos, deleteTodo, toggleTodo }) { | ||
| return ( | ||
| <div css={sectionStyle}> | ||
| <h3>{title} ({todos.length})</h3> | ||
| {todos.map((todo, index) => ( | ||
| <TodoItem | ||
| key={index} | ||
|
Comment on lines
+13
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
리스트 항목의 순서가 바뀌거나 항목이 추가/삭제될 때, 각 항목에 대응되는 즉,
참고자료 - React 공식문서 key 규칙 |
||
| index={index} | ||
| todo={todo} | ||
| deleteTodo={deleteTodo} | ||
| toggleTodo={toggleTodo} | ||
| /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default TodoListSection; | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
세세한 컴포넌트 분리로 코드가 깔끔합니다 👍