-
Notifications
You must be signed in to change notification settings - Fork 10
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
[2주차] 최지원 미션 제출합니다. #1
base: master
Are you sure you want to change the base?
Changes from all commits
528e288
69d0ea2
daf4ca1
63796c1
cbcc781
f49e107
c5a1884
3e29cdd
2bdff20
332efe0
8d572cf
67b3191
608f739
8c5f3fb
d307485
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 |
---|---|---|
|
@@ -21,3 +21,4 @@ | |
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
node_modules |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"singleQuote": true, | ||
"semi": true, | ||
"useTabs": false, | ||
"tabWidth": 2, | ||
"trailingComma": "all", | ||
"printWidth": 80, | ||
"bracketSpacing": true, | ||
"arrowParens": "always", | ||
"endOfLine": "auto" | ||
} |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,12 @@ | ||
import TodoTemplate from "./TodoTemplate"; | ||
import GlobalStyle from "./statics/GlobalStyle"; | ||
|
||
function App() { | ||
return ( | ||
<div className="App"> | ||
<h1>🐶CEOS 20기 프론트엔드 최고🐶</h1> | ||
</div> | ||
<> | ||
<GlobalStyle /> | ||
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. globla style을 잘 지정해주신 점 너무 좋아요👍👍 |
||
<TodoTemplate /> | ||
</> | ||
); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,117 @@ | ||||||||||||||||||||||||||||||||||
import React, { useCallback, useEffect, useState } from 'react'; | ||||||||||||||||||||||||||||||||||
import styled from 'styled-components'; | ||||||||||||||||||||||||||||||||||
import { S } from './components/Common.style'; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
import Navbar from './components/Navbar'; | ||||||||||||||||||||||||||||||||||
import TodoBoard from './components/TodoBoard'; | ||||||||||||||||||||||||||||||||||
import TodoInput from './components/TodoInput'; | ||||||||||||||||||||||||||||||||||
import DonutGraph from './components/DonutGraph'; | ||||||||||||||||||||||||||||||||||
import Footer from './components/Footer'; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const TodoTemplate = () => { | ||||||||||||||||||||||||||||||||||
// 초기 데이터 | ||||||||||||||||||||||||||||||||||
const [todos, setTodos] = useState(() => { | ||||||||||||||||||||||||||||||||||
const savedTodos = localStorage.getItem('todos'); | ||||||||||||||||||||||||||||||||||
return savedTodos ? JSON.parse(savedTodos) : []; | ||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// 배열이 변경될 때마다 localStorage 업데이트 | ||||||||||||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||||||||||||
localStorage.setItem('todos', JSON.stringify(todos)); | ||||||||||||||||||||||||||||||||||
}, [todos]); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// 항목 추가 | ||||||||||||||||||||||||||||||||||
const addItem = useCallback((text) => { | ||||||||||||||||||||||||||||||||||
const item = { | ||||||||||||||||||||||||||||||||||
id: Date.now().toString(), | ||||||||||||||||||||||||||||||||||
text, | ||||||||||||||||||||||||||||||||||
checked: false, | ||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||
setTodos((prevTodos) => [...prevTodos, item]); | ||||||||||||||||||||||||||||||||||
}, []); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// 항목 삭제 | ||||||||||||||||||||||||||||||||||
const removeItem = useCallback((id) => { | ||||||||||||||||||||||||||||||||||
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id)); | ||||||||||||||||||||||||||||||||||
}, []); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// 항목 토글 | ||||||||||||||||||||||||||||||||||
const toggleItem = useCallback((id) => { | ||||||||||||||||||||||||||||||||||
setTodos((prevTodos) => | ||||||||||||||||||||||||||||||||||
prevTodos.map((todo) => | ||||||||||||||||||||||||||||||||||
todo.id === id | ||||||||||||||||||||||||||||||||||
? { ...todo, checked: !todo.checked, id: Date.now().toString() } | ||||||||||||||||||||||||||||||||||
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. 여기에서 todo가 toggle된 경우에 이런 부분은 주석으로 남겨주시면 좋을 것 같아요🥰 아니면 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.
|
||||||||||||||||||||||||||||||||||
: todo, | ||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||
}, []); | ||||||||||||||||||||||||||||||||||
Comment on lines
+39
to
+47
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. 시연할때 todo 정렬하는 기능은 못찾았는데, 없는게 맞는거라면 checked 상태를 토글할 때마다 id를 새로운 값으로 변경하는 것은 없애는 것이 예상치 못한 버그가 발생을 방지할 것 같습니다!
Suggested change
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. 처음엔 체크를 토글할 때 id를 업데이트하지 않았었는데, 그렇게 되면 추가했던 순서대로 todo id가 정렬되어서 done에 갔다가 다시 돌아올 때 중간에 항목이 끼어들어가는 게 조금 어색하게 느껴지더라구요..! 토글할 때마다 아래쪽으로 밀리도록 의도한 것은 맞으나 말씀해주신 대로 버그를 고려하면 lastUpdated 라는 속성을 따로 두어 업데이트 시키고 id는 고유하게 두면 좋겠다는 생각이 듭니다! 감사합니다😊 |
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// 입력창 열고 닫음 | ||||||||||||||||||||||||||||||||||
const [isFormOpen, setIsFormOpen] = useState(false); | ||||||||||||||||||||||||||||||||||
const [animationClassname, setAnimationClassname] = useState(''); // 애니메이션 지정을 위한 클래스명 | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const toggleForm = () => { | ||||||||||||||||||||||||||||||||||
// 닫힘 시 애니메이션 시간만큼의 지연 필요 | ||||||||||||||||||||||||||||||||||
const timer = () => | ||||||||||||||||||||||||||||||||||
setTimeout(() => { | ||||||||||||||||||||||||||||||||||
setIsFormOpen(!isFormOpen); | ||||||||||||||||||||||||||||||||||
}, 300); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
if (isFormOpen) { | ||||||||||||||||||||||||||||||||||
setAnimationClassname('fade-out'); | ||||||||||||||||||||||||||||||||||
timer(); | ||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||
setAnimationClassname('fade-in'); | ||||||||||||||||||||||||||||||||||
setIsFormOpen(!isFormOpen); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
return () => clearTimeout(timer); | ||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||
Comment on lines
+53
to
+68
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. timer를 이용한 애니메이션 관리 너무 좋아요👍👍 |
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// 항목 개수 | ||||||||||||||||||||||||||||||||||
const totalCount = todos.length; // 전체 항목 | ||||||||||||||||||||||||||||||||||
const doneCount = todos.reduce((count, todo) => { | ||||||||||||||||||||||||||||||||||
return todo.checked ? count + 1 : count; | ||||||||||||||||||||||||||||||||||
}, 0); // 완료 항목 | ||||||||||||||||||||||||||||||||||
Comment on lines
+71
to
+74
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 percent = (doneCount / totalCount) * 100; // 도넛그래프 성취율 | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||
<Wrapper> | ||||||||||||||||||||||||||||||||||
<Navbar {...{ isFormOpen, toggleForm }} /> | ||||||||||||||||||||||||||||||||||
<Container> | ||||||||||||||||||||||||||||||||||
<TodoBoard {...{ todos, removeItem, toggleItem }} /> | ||||||||||||||||||||||||||||||||||
<InputAndGraph> | ||||||||||||||||||||||||||||||||||
{isFormOpen && <TodoInput {...{ animationClassname, addItem }} />} | ||||||||||||||||||||||||||||||||||
<DonutGraph {...{ percent }} /> | ||||||||||||||||||||||||||||||||||
</InputAndGraph> | ||||||||||||||||||||||||||||||||||
</Container> | ||||||||||||||||||||||||||||||||||
<Footer {...{ totalCount, doneCount }} /> | ||||||||||||||||||||||||||||||||||
</Wrapper> | ||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
export default TodoTemplate; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const Wrapper = styled.div` | ||||||||||||||||||||||||||||||||||
display: flex; | ||||||||||||||||||||||||||||||||||
flex-direction: column; | ||||||||||||||||||||||||||||||||||
align-items: center; | ||||||||||||||||||||||||||||||||||
width: 40rem; | ||||||||||||||||||||||||||||||||||
max-width: 100%; | ||||||||||||||||||||||||||||||||||
height: 100%; | ||||||||||||||||||||||||||||||||||
`; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const Container = styled(S.Box)` | ||||||||||||||||||||||||||||||||||
display: grid; | ||||||||||||||||||||||||||||||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); | ||||||||||||||||||||||||||||||||||
gap: 10px; | ||||||||||||||||||||||||||||||||||
width: 100%; | ||||||||||||||||||||||||||||||||||
height: auto; | ||||||||||||||||||||||||||||||||||
padding: 0.625rem 1.25rem; | ||||||||||||||||||||||||||||||||||
`; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const InputAndGraph = styled.div` | ||||||||||||||||||||||||||||||||||
display: flex; | ||||||||||||||||||||||||||||||||||
flex-direction: column; | ||||||||||||||||||||||||||||||||||
align-items: flex-end; | ||||||||||||||||||||||||||||||||||
height: 100%; | ||||||||||||||||||||||||||||||||||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import styled from "styled-components"; | ||
|
||
const Ment = styled.div` | ||
color: var(--blue); | ||
font-weight: 600; | ||
font-size: 1.125rem; | ||
`; | ||
|
||
const Bold = styled.div` | ||
color: var(--light-blue); | ||
font-weight: 600; | ||
font-size: 0.938rem; | ||
`; | ||
|
||
const Box = styled.div` | ||
border-radius: 1.875rem; | ||
border: 0.063rem solid var(--blue); | ||
`; | ||
|
||
export const S = { | ||
Ment, | ||
Box, | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,45 @@ | ||||||||||||
import React from "react"; | ||||||||||||
import styled from "styled-components"; | ||||||||||||
|
||||||||||||
import { CircularProgressbar } from "react-circular-progressbar"; | ||||||||||||
import "react-circular-progressbar/dist/styles.css"; | ||||||||||||
|
||||||||||||
const DonutGraph = React.memo(({ percent }) => { | ||||||||||||
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.memo는 컴포넌트가 동일한 props로 여러 번 렌더링되는 것을 방지하기 위해 사용하는데, 이 컴포넌트는 부모 컴포넌트에서 props가 변경될 때만 다시 렌더링되어서 React.memo를 안사용하는 것이 성능측면에서 더 좋을 것 같아요! 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. 새로운 props가 들어오지 않더라도 부모컴포넌트가 리렌더링 된다면 자식컴포넌트도 리렌더링 된다고 알고 있습니다..! 인풋 창을 여닫을 때마다 TodoTemplate가 리렌더링 되고, 그 하위 컴포넌트인 DonutGraph도 렌더링이 되더라구요 !! |
||||||||||||
// todos 항목이 없는 빈 배열일 경우 0%로 초기화 | ||||||||||||
if (isNaN(percent)) { | ||||||||||||
percent = 0; | ||||||||||||
} | ||||||||||||
Comment on lines
+9
to
+11
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. props를 자식컴포넌트에서 직접 수정하는 것보다 이렇게 변수를 만들어서 하는 것도 좋은 방법일 것 같아요
Suggested change
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 ( | ||||||||||||
<Wrapper> | ||||||||||||
<CircularProgressbar value={percent} text={`${Math.round(percent)}%`} /> | ||||||||||||
</Wrapper> | ||||||||||||
); | ||||||||||||
}); | ||||||||||||
|
||||||||||||
export default DonutGraph; | ||||||||||||
|
||||||||||||
const Wrapper = styled.div` | ||||||||||||
display: flex; | ||||||||||||
flex-grow: 1; | ||||||||||||
align-items: center; | ||||||||||||
width: 10rem; | ||||||||||||
margin: 0.625rem 1.25rem 1.25rem 0; | ||||||||||||
|
||||||||||||
@media (max-width: 600px) { | ||||||||||||
width: 80%; | ||||||||||||
} | ||||||||||||
|
||||||||||||
.CircularProgressbar-path { | ||||||||||||
stroke: #5f81ff; | ||||||||||||
} | ||||||||||||
|
||||||||||||
.CircularProgressbar-trail { | ||||||||||||
stroke: #dfe8ff; | ||||||||||||
} | ||||||||||||
|
||||||||||||
.CircularProgressbar-text { | ||||||||||||
fill: #000; | ||||||||||||
font-weight: 600; | ||||||||||||
} | ||||||||||||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import React from "react"; | ||
import styled from "styled-components"; | ||
|
||
const Footer = React.memo(({ totalCount, doneCount }) => { | ||
return ( | ||
<Wrapper> | ||
<Span>Total: {totalCount}</Span> | ||
<Span> | ||
Accomplishment: {doneCount}/{totalCount} | ||
</Span> | ||
</Wrapper> | ||
); | ||
}); | ||
|
||
export default Footer; | ||
|
||
const Wrapper = styled.div` | ||
width: 14rem; | ||
display: flex; | ||
flex-direction: row; | ||
justify-content: space-between; | ||
align-self: flex-start; | ||
margin: 0.625rem 0 0 1.25rem; | ||
`; | ||
|
||
const Span = styled.span` | ||
color: var(--light-blue); | ||
font-weight: 300; | ||
font-size: 0.938rem; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import React from "react"; | ||
import styled from "styled-components"; | ||
|
||
import { ReactComponent as EmptyIcon } from "../images/empty_checkbox.svg"; | ||
import { ReactComponent as FullIcon } from "../images/full_checkbox.svg"; | ||
import { ReactComponent as DeleteIcon } from "../images/delete_btn.svg"; | ||
|
||
const Item = ({ todo, removeItem, toggleItem }) => { | ||
const { id, text, checked } = todo; | ||
|
||
return ( | ||
<Wrapper> | ||
<CheckButton onClick={() => toggleItem(id)}> | ||
{checked ? <FullIcon /> : <EmptyIcon />} | ||
</CheckButton> | ||
<ItemText>{text}</ItemText> | ||
<DeleteButton onClick={() => removeItem(id)} /> | ||
</Wrapper> | ||
); | ||
}; | ||
|
||
export default Item; | ||
|
||
const Wrapper = styled.li` | ||
display: flex; | ||
align-items: center; | ||
margin: 0 0 0.938rem 0.5rem; | ||
`; | ||
|
||
const ItemText = styled.span` | ||
color: var(--blue); | ||
font-weight: 300; | ||
font-size: 1rem; | ||
word-break: break-all; | ||
max-width: 100%; | ||
`; | ||
|
||
const CheckButton = styled.span` | ||
display: block; | ||
width: 1.25rem; | ||
height: 1.25rem; | ||
margin-right: 0.6rem; | ||
flex-shrink: 0; | ||
`; | ||
|
||
const DeleteButton = styled(DeleteIcon)` | ||
width: 0.688rem; | ||
margin-left: 0.4rem; | ||
flex-shrink: 0; | ||
`; |
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.
원형 상태바 라이브러리 활용한 것 좋은 것 같아요. 하나 알아갑니다 ㅎㅎ