Skip to content
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

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
node_modules
11 changes: 11 additions & 0 deletions .prettierrc
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"
}
500 changes: 433 additions & 67 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"normalize.css": "^8.0.1",
"prettier": "^3.3.3",
"react": "^18.3.1",
"react-circular-progressbar": "^2.1.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원형 상태바 라이브러리 활용한 것 좋은 것 같아요. 하나 알아갑니다 ㅎㅎ

"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
"styled-components": "^6.1.13",
"web-vitals": "^2.1.4"
},
"scripts": {
Expand Down
Binary file added public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 2 additions & 33 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,11 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="./favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.

Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>TodoList</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.

To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
10 changes: 7 additions & 3 deletions src/App.js
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 />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

globla style을 잘 지정해주신 점 너무 좋아요👍👍

<TodoTemplate />
</>
);
}

Expand Down
117 changes: 117 additions & 0 deletions src/TodoTemplate.jsx
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() }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에서 todo가 toggle된 경우에 id를 새로 생성하셨네요! 처음 코드를 읽을 때는 고유해야 할 것 같은 id가 변경되어서 조금 어색하게 느껴졌어요. 나중에 todo를 정렬하기 위해서 id를 변경한다는 걸 보고 이해했습니다!

이런 부분은 주석으로 남겨주시면 좋을 것 같아요🥰 아니면 id와 별개로 lastModifiedAt같은 속성을 추가해서 관리하는 건 어떠신지 궁금해요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastModifiedAt으로 추가하는 방식 좋을 것 같네요!! id는 고유 정수로 통용되는 개념이다보니 별개로 둔 속성을 정렬에 적용하는게 더 낫겠다는 생각이 듭니다! 감사해요👍

: todo,
),
);
}, []);
Comment on lines +39 to +47
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시연할때 todo 정렬하는 기능은 못찾았는데, 없는게 맞는거라면 checked 상태를 토글할 때마다 id를 새로운 값으로 변경하는 것은 없애는 것이 예상치 못한 버그가 발생을 방지할 것 같습니다!

Suggested change
const toggleItem = useCallback((id) => {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id
? { ...todo, checked: !todo.checked, id: Date.now().toString() }
: todo,
),
);
}, []);
const toggleItem = useCallback((id) => {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, checked: !todo.checked } : todo,
),
);
}, []);

Copy link
Author

@jiwonnchoi jiwonnchoi Sep 23, 2024

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduce를 이용해서 함수형 프로그래밍의 원칙을 따르는 점이 너무 좋아요 ;)

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%;
`;
23 changes: 23 additions & 0 deletions src/components/Common.style.js
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,
};
45 changes: 45 additions & 0 deletions src/components/DonutGraph.jsx
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 }) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React.memo는 컴포넌트가 동일한 props로 여러 번 렌더링되는 것을 방지하기 위해 사용하는데, 이 컴포넌트는 부모 컴포넌트에서 props가 변경될 때만 다시 렌더링되어서 React.memo를 안사용하는 것이 성능측면에서 더 좋을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

새로운 props가 들어오지 않더라도 부모컴포넌트가 리렌더링 된다면 자식컴포넌트도 리렌더링 된다고 알고 있습니다..! 인풋 창을 여닫을 때마다 TodoTemplate가 리렌더링 되고, 그 하위 컴포넌트인 DonutGraph도 렌더링이 되더라구요 !!
https://velog.io/@ehdxka3/%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81%EC%9D%B4-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-%EC%A1%B0%EA%B1%B4React

// todos 항목이 없는 빈 배열일 경우 0%로 초기화
if (isNaN(percent)) {
percent = 0;
}
Comment on lines +9 to +11
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

props를 자식컴포넌트에서 직접 수정하는 것보다 이렇게 변수를 만들어서 하는 것도 좋은 방법일 것 같아요

Suggested change
if (isNaN(percent)) {
percent = 0;
}
const displayPercent = isNaN(percent) ? 0 : percent;

Copy link
Author

Choose a reason for hiding this comment

The 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;
}
`;
30 changes: 30 additions & 0 deletions src/components/Footer.jsx
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;
`;
50 changes: 50 additions & 0 deletions src/components/Item.jsx
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;
`;
Loading