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주차] 조유담 미션 제출합니다 #8

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
15 changes: 15 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime'],
overrides: [],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['react'],
rules: { 'react/prop-types': 'off', 'no-unused-vars': 'warn' },
};
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"printWidth": 120,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all",
"semi": true
}
5 changes: 5 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"baseUrl": "src"
}
}
Copy link

Choose a reason for hiding this comment

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

절대경로를 설정해주셔서 매번 상대 경로를 작성하어 import하는 것보다 편리한 것 같아요!

Choose a reason for hiding this comment

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

절대 경로 설정 배워갑니다! 다음 과제부터 적용해봐야겠어요!

5,419 changes: 2,576 additions & 2,843 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.0.1",
"react-scripts": "5.0.1",
"styled-components": "^6.1.8",
"uuid": "^9.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
Expand All @@ -34,5 +37,11 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"babel-plugin-styled-components": "^2.1.4",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.2.5"
}
}
43 changes: 9 additions & 34 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -1,43 +1,18 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<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.
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TODO-LIST</title>
<link rel="stylesheet" href="./style/style.css" />

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>
<meta property="og:title" content="TODO-LIST" />
<meta property="og:url" content="https://vanilla-todo-19th-navy.vercel.app/" />
<meta property="og:type" content="website" />
<meta property="og:description" content="오늘의 일정을 관리해보세요." />
</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>
9 changes: 0 additions & 9 deletions src/App.js

This file was deleted.

55 changes: 55 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import TodoCreate from 'components/TodoCreate';
import TodoHeader from 'components/TodoHeader';
import TodoList from 'components/TodoList';
import { useEffect, useReducer } from 'react';
import todoReducer from 'reducer/todoReducer';
import styled from 'styled-components';
import { flexCenter, flexColumn } from 'styles/commonStyle';

const initialState = {
todoList: JSON.parse(localStorage.getItem('todoList')) || [],
doneList: JSON.parse(localStorage.getItem('doneList')) || [],
};

function App() {
const [state, dispatch] = useReducer(todoReducer, initialState);
const { todoList, doneList } = state;

useEffect(() => {
localStorage.setItem('todoList', JSON.stringify(todoList));
localStorage.setItem('doneList', JSON.stringify(doneList));
}, [todoList]);

Choose a reason for hiding this comment

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

의존성 배열을 todoList만 설정해주셨다 보니 doneList에 있는 아이템을 삭제하면 localStorage에 반영이 안되는 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

시간이 없어서 QA를 제대로 못한 잘못이네요 ㅠㅠㅋㅋ done<->todo로 이동할 때만 doneList가 바뀐다고 생각해서 todoList만 의존성 배열에 넣어줬네요 ! 삭제를 고려 못했어요 감사합니다!


return (
<TodoAppLayout>
<TodoHeader />
<TodoMain>
<TodoCreate list={todoList} dispatch={dispatch} />
<TodoListContainer>
<TodoList listName="todo" list={todoList} dispatch={dispatch} />
<TodoList listName="done" list={doneList} dispatch={dispatch} />
</TodoListContainer>
</TodoMain>
</TodoAppLayout>
);
}

export default App;

const TodoAppLayout = styled.div`
padding: 4rem 0;
gap: 2rem;
${flexCenter}
${flexColumn}
`;

const TodoMain = styled.main`
${flexColumn}
gap: 3rem;
`;

const TodoListContainer = styled.section`
display: flex;
width: 100%;
justify-content: space-between;
`;
96 changes: 96 additions & 0 deletions src/components/TodoCreate.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useState } from 'react';
import styled from 'styled-components';
import { flexCenter, flexColumn } from 'styles/commonStyle';
import { v4 as uuid } from 'uuid';

function TodoCreate({ dispatch }) {
const [inputValue, setInputValue] = useState('');
const [isError, setIsError] = useState(false);

const handleSubmit = (e) => {
e.preventDefault();
if (!inputValue?.trim()) {
setIsError(true);
return;
}

dispatch({
type: 'ADD_TODO',
payload: {
id: uuid(),
Copy link

Choose a reason for hiding this comment

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

저는 Date.now()로 현재 시간을 ID로 지정해줬는데요..!
동시에 여러 ID를 생성해야 하는 경우가 있다면 uuid 함수를 사용하는 것이 고유성 측면에서 더 적합한 것 같네요. 유담님 코드 보고 배우고 갑니다!

text: inputValue,
},
});

setIsError(false);
setInputValue('');
};

const handleInputValueChange = (e) => {
setInputValue(e.target.value);
};

return (
<InputForm onSubmit={handleSubmit}>
<InputContainer>
<InputWrapper>
<input className="input" placeholder="할 일 입력" value={inputValue} onChange={handleInputValueChange} />
<button className="input-button">입력</button>
</InputWrapper>
<ErrorMessage $isError={isError}>할 일을 입력해주세요</ErrorMessage>

Choose a reason for hiding this comment

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

입력값이 없을 때 에러 메시지 띄우는 디테일 너무 좋네요~👍🏻

</InputContainer>
</InputForm>
);
}

export default TodoCreate;

const InputForm = styled.form`
display: flex;

Choose a reason for hiding this comment

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

${flexCenter}에 display: flex가 이미 있어서 요 부분은 빼줘도 될 것 같아요!

width: 100%;
height: 6rem;
${flexCenter}
`;

const InputContainer = styled.div`
${flexColumn}
height: 100%;
gap: 1rem;
`;

Choose a reason for hiding this comment

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

InputFormInputContainer를 분리하신 이유가 있을까요? 둘을 하나로 합쳐도 될 것 같다는 생각이 드는데 혹시 제가 놓친 유담님의 의도가 있다면 말씀해주세요!

Copy link
Author

Choose a reason for hiding this comment

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

인풋 + 버튼은 가로 방향으로 , 이 둘을 감싸고 있는 InputWrapper와 ErrorMessage는 세로 방향으로 배치가 되어야해서요..!!


const InputWrapper = styled.div`
display: flex;
height: 100%;
gap: 2rem;

& {
font-size: 2.5rem;
}

.input {
width: 50rem;
padding: 1rem;
border-radius: 1rem;
}
Comment on lines +69 to +73

Choose a reason for hiding this comment

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

이런식으로 영어가 길게 입력되었을 때 깨지고 있어요!
word-break: break-all로 해결할 수 있답니당

스크린샷 2024-03-24 오전 1 06 32

Suggested change
.input {
width: 50rem;
padding: 1rem;
border-radius: 1rem;
}
.input {
width: 50rem;
padding: 1rem;
border-radius: 1rem;
word-break: break-all;
}


.input:focus {
border: 0.1rem solid #6d6afe;
}

.input-button {
width: 8rem;
padding: 0.2rem;
background: linear-gradient(91deg, #6d6afe 0.12%, #6ae3fe 101.84%);
color: white;
font-weight: 200;
border-radius: 1rem;
}
`;

const ErrorMessage = styled.p`
color: red;
font-size: 1.7rem;
margin-left: 0.4rem;

visibility: ${({ $isError }) => ($isError ? 'visible' : 'hidden')};
`;
Copy link

Choose a reason for hiding this comment

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

조건부 연산자를 이용한 동적 스타일링 좋은 것 같아요~

40 changes: 40 additions & 0 deletions src/components/TodoHeader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useEffect, useState } from 'react';
import styled from 'styled-components';
import { getTodayDate } from 'util/getTodayDate';

function TodoHeader() {
const [userName, setUserName] = useState(localStorage.getItem('userName') ?? '');

Choose a reason for hiding this comment

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

저는 nullish 병합 연산자 ?? 대신에 ||를 사용했는데, 이런 방법도 있었군요!
둘의 차이점에 대해 알아두는게 좋을 것 같아 링크 남기고 갑니다!


// 사용자 이름 설정
const showUserName = () => {
if (!userName) {
const nameInput = prompt('이름을 알려주세요!');
localStorage.setItem('userName', nameInput);
setUserName(nameInput);

Choose a reason for hiding this comment

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

사용자 이름 설정 기능 신박하고 좋네요~~

}
};
Copy link

Choose a reason for hiding this comment

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

사용자 이름 설정 기능까지 추가로 구현하시다니..!
그런데 사용자 이름 설정에서 취소 버튼을 누르면 localStorage에 문자열 null로 저장이 되어서요,
사용자가 취소 버튼을 눌렀을 때 null을 반환하는 것을 감지하고, 이 경우 localStorage에 값을 저장하지 않으며, 이름 입력을 다시 요청하는 로직이 있으면 좋을 것 같아요! 공백 예외 처리도 있으면 좋을 것 같아요~


useEffect(() => {
showUserName();
});

Choose a reason for hiding this comment

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

현재 코드에서는 showUserName 함수가 매 렌더링마다 실행되도록 되어있는데요, 의존성 배열에 빈 배열을 설정하는 대신 이렇게 작성한 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

실수입니다..!! 감사해요 ㅎㅎ


return (
<HeaderWrapper>
<p>{getTodayDate()}</p>
<h1>
<span>{userName}이의 </span>TODO-LIST
</h1>
</HeaderWrapper>
);
}

export default TodoHeader;

const HeaderWrapper = styled.header`
display: flex;
font-size: 2.5rem;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
`;
72 changes: 72 additions & 0 deletions src/components/TodoList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import styled from 'styled-components';
import { flexCenter, flexColumn } from 'styles/commonStyle';
import { FaRegCheckCircle } from 'react-icons/fa';
import { FaRegCircle } from 'react-icons/fa';
import { FaRegTrashAlt } from 'react-icons/fa';
Copy link

Choose a reason for hiding this comment

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

저는 항상 이미지로 사용했는데 유담님처럼 react-icons 라이브러리를 사용하는 방법도 좋은 것 같아요!
크게 차이는 없지만 위 코드 import { flexCenter, flexColumn } from 'styles/commonStyle'; 같이 세 개 아이콘 한번에 import해도 좋을 것 같아요!


function TodoList({ listName, list, dispatch }) {
const handleListItemClick = ({ id, text }) => {
const actionType = listName === 'todo' ? 'MOVE_TODO_TO_DONE' : 'MOVE_DONE_TO_TODO';
dispatch({ type: actionType, payload: { id, text } });
};

const handleDeleteItem = (e, { id, text }) => {
e.stopPropagation();
const actionType = listName === 'todo' ? 'REMOVE_TODO' : 'REMOVE_DONE';
dispatch({ type: actionType, payload: { id, text } });
};

return (
<TodoListWrapper>
<h2>
{listName} <span> / {list.length}개</span>
</h2>
<TodoListContainer>
{list.map((li) => (
<TodoListItem key={li.id} onClick={() => handleListItemClick(li)}>
{listName == 'todo' ? <FaRegCircle /> : <FaRegCheckCircle />}
{li.text}
<FaRegTrashAlt className="trash-icon" onClick={(e) => handleDeleteItem(e, li)} />
</TodoListItem>
))}
</TodoListContainer>
</TodoListWrapper>
);
}

export default TodoList;

const TodoListWrapper = styled.article`
display: flex;
width: 28rem;
height: 55rem;
padding: 2rem;
border: 0.13rem solid #6d6afe;
gap: 1rem;
align-items: center;
flex-direction: column;
border-radius: 2rem;

& {
font-size: 2.5rem;
}
`;

const TodoListContainer = styled.ul`
${flexColumn}
gap: 1rem;
overflow: auto;
`;

const TodoListItem = styled.li`
display: flex;
${flexCenter}
gap: 1rem;
cursor: pointer;
.trash-icon {
visibility: hidden;
}
&:hover .trash-icon {
visibility: visible;
}

Choose a reason for hiding this comment

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

할 일이 길게 입력되면 이런식으로 아이콘이 작아지는 것 같아요!
할 일 텍스트에 max-width를 설정하거나 아이콘 크기를 고정해서 해결하면 될 것 같습니다:)
스크린샷 2024-03-24 오전 12 47 41

`;
Binary file added src/font/meetme.woff
Binary file not shown.
Loading