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주차] 고윤정 미션 제출합니다. #2

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
17 changes: 11 additions & 6 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import js from '@eslint/js';
import globals from 'globals';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';

export default [
{ ignores: ['dist'] },
Expand Down Expand Up @@ -35,4 +35,9 @@ export default [
],
},
},
]
{
rules: {
'react/no-unknown-property': ['error', { ignore: ['css'] }],
},
},
];
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.13.3",
"normalize.css": "^8.0.1",
Copy link
Member

Choose a reason for hiding this comment

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

normalize 좋습니다!

Copy link

@Sieonn Sieonn Sep 5, 2024

Choose a reason for hiding this comment

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

오? normalize.css는 처음봐서 찾아보니 정규화를 시켜주고 다른 브라우저로 보더라도 일정한 디자인을 볼 수 있도록 해주기위해서 사용한다고 하는데 좋은거 알아갑니다!!!

"prop-types": "^15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand Down
82 changes: 81 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,85 @@
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import 'normalize.css';
Copy link
Member

Choose a reason for hiding this comment

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

전역적으로 스타일을 적용하고 싶다면 Emotion의 Global 컴포넌트도 사용해 보세요!
참고 자료 - Emotion Global Styles

import { useEffect, useState } from 'react';
import InputBox from './components/InputBox.jsx';
import ToDoItemList from './components/ToDoItemList.jsx';

function App() {
return <div>React Todo</div>;
const bodyStyle = css`
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-image: linear-gradient(
to bottom right,
rgb(247, 196, 218),
rgb(239, 239, 239)
);
`;
Comment on lines +9 to +20
Copy link
Member

Choose a reason for hiding this comment

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

스타일 코드는 컴포넌트(App) 바깥에 작성해 주시면 가독성이 더 좋아질 것 같아요 😀

export default function App() {
   // 생략
}

const wrapper = css`
   // 생략
`;

Copy link
Member Author

Choose a reason for hiding this comment

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

뺄지 말지 고민했던 부분 중 하나인데 확실하게 말씀해주셔서 좋아요ㅎ 당장 밖으로 빼겠습니다🫡


const containerStyle = css`
width: 350px;
height: 600px;
background-color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 20px;
padding: 20px;
`;

const mainTitleStyle = css`
@font-face {
font-family: 'LINESeedKR-Bd';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/[email protected]/LINESeedKR-Bd.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
}
font-family: 'LINESeedKR-Bd';
color: rgb(59, 56, 56);
`;

// 초기 상태를 localStorage에서 불러옴
const [todoList, setTodoList] = useState(() => {
const savedTodoList = localStorage.getItem('todoList');
return savedTodoList ? JSON.parse(savedTodoList) : [];
});
Comment on lines +43 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.

👍🏻👍🏻

Copy link

Choose a reason for hiding this comment

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

오 엄청 간결하면서도 동작이 잘 보여요!!
이름을 엄청 잘 지으시는 것 같어요


// todoList가 업데이트될 때마다 localStorage에 저장
useEffect(() => {
localStorage.setItem('todoList', JSON.stringify(todoList));
}, [todoList]);

return (
<div className="body" css={bodyStyle}>
Copy link
Member

Choose a reason for hiding this comment

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

클래스명을 부여하신 이유가 있으신가요?!

<div className="container" css={containerStyle}>
<h2 className="title" css={mainTitleStyle}>
Comment on lines +56 to +57
Copy link
Member

Choose a reason for hiding this comment

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

emotion을 잘 적용해 주셔서 코드가 매우 간결하네요!

To Do List
</h2>
{/* 할 일 입력 */}
<InputBox todoList={todoList} setTodoList={setTodoList} />

{/* 할 일 목록 */}
<ToDoItemList
title={`📂 TO DO (${
todoList.filter((item) => !item.checked).length
})`} // 아직 완료되지 않은목록의 길이
Comment on lines +65 to +67
Copy link
Member

Choose a reason for hiding this comment

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

filter() 메서드와 템플릿 리터럴 사용까지 아주 좋습니다!
다만 아래처럼 미리 변수에 할당하고 props로 넘기는 게 가독성이 더욱 좋아질 것 같습니다 🙂

const todoCount = todoList.filter((item) => !item.checked).length;
Suggested change
title={`📂 TO DO (${
todoList.filter((item) => !item.checked).length
})`} // 아직 완료되지 않은목록의 길이
title={`📂 TO DO (${todoCount})`} // 아직 완료되지 않은목록의 길이

todoList={todoList}
setTodoList={setTodoList}
checkedList={false}
/>

{/* 완료된 목록 */}
<ToDoItemList
title={`🗑️ DONE (${todoList.filter((item) => item.checked).length})`} // 완료된 목록의 길이
todoList={todoList}
setTodoList={setTodoList}
checkedList={true}
/>
</div>
</div>
);
}

export default App;
111 changes: 111 additions & 0 deletions src/components/InputBox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import 'normalize.css';
import { useRef, useState } from 'react';
import PropTypes from 'prop-types';

export default function InputBox({ todoList, setTodoList }) {
const formStyle = css`
display: flex;
align-items: center;
`;

const inputStyle = css`
width: 300px;
height: 35px;
outline: none;
border-radius: 20px;
border: 1.2px solid;
border-color: rgb(247, 196, 218);
padding-left: 12px;
@font-face {
font-family: 'LINESeedKR-Rg';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/[email protected]/LINESeedKR-Rg.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
}
Comment on lines +21 to +27
Copy link
Member

Choose a reason for hiding this comment

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

normalize.css를 비롯하여 이렇게 @font-face를 정의하는 부분도 글로벌 스타일로 적용해 보시면 좋을 것 같아요!

font-family: 'LINESeedKR-Rg';
font-size: 12px;
color: #3d3d3d;
`;

const addButtonStyle = css`
border: 0;
background-color: white;
font-size: 20px;
color: rgb(59, 56, 56);
margin-left: 8px;
cursor: pointer;
&:hover {
color: rgb(247, 196, 218);
}
`;

const [text, setText] = useState(''); // input에 입력한 값
const inputRef = useRef(null);
Copy link

Choose a reason for hiding this comment

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

저는 form을 사용하지 않아서 form을 사용했을 때 새로고침을 방지해야하는군요!! 배워갑니다!!!


// form 제출 시 새로고침 방지
const formClickEvent = (e) => {
e.preventDefault();
};

// input 값 가져오기
function onChangeInput(e) {
setText(e.target.value);
// e.target에 있는 <input.../>으로부터 value 값을 가져옴
}
Comment on lines +53 to +57
Copy link
Member

Choose a reason for hiding this comment

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

이벤트 핸들러의 이름은 보통 handle + 대상 + 동작 순으로 짓습니다! (e.g. handleInputChange)


// + 버튼 클릭(form 제출)
function onClickButton() {
// 공백 입력 방지
if (text.trim() === '') return;
Copy link
Member

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.

캬.. 공백 방지까지...디테일의 은혜가 끝이 없군요


// todoItemList에 값 추가
const AddTodoList = todoList.concat({
id: todoList.length,
text,
checked: false,
});
Comment on lines +65 to +69
Copy link
Member

Choose a reason for hiding this comment

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

TodoList 배열 안의 객체를 식별하는 id를 length로 주신 이유가 있을까요? 만약 id가 같다면 원하지 않는 다른 객체가 수정될 수 있을 것 같아요! Date.now()와 같이 중복되지 않는 값을 id에 할당하면 좋을 것 같습니다!

setTodoList(AddTodoList);
Comment on lines +65 to +70
Copy link
Member

Choose a reason for hiding this comment

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

concat 메서드 사용 좋습니다! spread 연산자도 활용해 봅시다!

Suggested change
const AddTodoList = todoList.concat({
id: todoList.length,
text,
checked: false,
});
setTodoList(AddTodoList);
setTodoList(prev => [
...prev,
{
id: todoList.length,
text,
checked: false,
},
]);


setText(''); // input 값 초기화
inputRef.current.focus(); // 버튼 누른 후에도 input box에 자동 포커싱
Copy link
Member

Choose a reason for hiding this comment

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

useRef를 이용해서 input에 포커스를 주는 부분 새롭게 알아갑니다👀👀

}

return (
<div>
<form onSubmit={formClickEvent} className="form" css={formStyle}>
<input
type="text"
name="todoItem"
value={text}
ref={inputRef}
className="input-box"
placeholder="할일을 입력하세요"
Copy link

Choose a reason for hiding this comment

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

이런 placeholder하나만으로도 이 사이트의 정체성을 보여주는 것 같아요. 🦖 친절한 사이트...

onChange={onChangeInput} // input 값이 변하면(이벤트 발생) 메소드 실행
css={inputStyle}
autoFocus
/>
<button
className="add-button"
onClick={onClickButton}
css={addButtonStyle}
>
+
</button>
</form>
</div>
);
}

// props 값 검증
InputBox.propTypes = {
Copy link
Member

Choose a reason for hiding this comment

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

나중에 타입스크립트도 잘하실 것 같아요 😉

todoList: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
}).isRequired
),
setTodoList: PropTypes.func.isRequired,
};
Comment on lines +104 to +111
Copy link
Member

Choose a reason for hiding this comment

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

오 props 값을 이렇게 검증할 수 있군요! 새로 알아갑니다👀

126 changes: 126 additions & 0 deletions src/components/ToDoItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import 'normalize.css';
import PropTypes from 'prop-types';

export default function ToDoItem({ todoItem, todoList, setTodoList }) {
const spanStyle = css`
@font-face {
font-family: 'LINESeedKR-Rg';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/[email protected]/LINESeedKR-Rg.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
}
font-family: 'LINESeedKR-Rg';
font-size: 15px;
color: rgb(59, 56, 56);
`;

const spanCheckedStyle = css`
@font-face {
font-family: 'LINESeedKR-Rg';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/[email protected]/LINESeedKR-Rg.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
}
font-family: 'LINESeedKR-Rg';
font-size: 15px;
color: gray;
text-decoration: line-through;
`;

const liStyle = css`
margin: 3px 0px 3px 0px;
`;

const checkBoxStyle = css`
Copy link

@Sieonn Sieonn Sep 5, 2024

Choose a reason for hiding this comment

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

이렇게 체크박스까지 디테일을 놓치지 않는 것이 디자인적으로 완성도가 매우 높은 것 같습니다.👍🏻

cursor: pointer;
appearance: none;
width: 12px;
height: 12px;
margin-right: 10px;
border: 1px solid;
border-radius: 10px;
border-color: rgb(59, 56, 56);

&:checked {
border-color: transparent;
background-size: 100% 100%;
background-color: rgb(247, 196, 218);
}
`;

const deleteButtonStyle = css`
border: 0;
background-color: white;
font-size: 14px;
color: rgb(59, 56, 56);
margin-left: 3px;
cursor: pointer;
&:hover {
color: rgb(247, 196, 218);
}
`;

// checkbox를 클릭하면, todoItem의 checked 값이 토글됨
function onChangeCheckbox() {
const updatedTodoList = todoList.map((item) => ({
...item,
checked: item.id === todoItem.id ? !item.checked : item.checked,
}));

setTodoList(updatedTodoList);
}

// 항목 삭제
function onDelete(id) {
// 주어진 id와 일치하지 않는 항목들만 남김(일치하면 필터링 -> 해당 항목 삭제)
const updatedTodoList = todoList.filter((todoItem) => todoItem.id !== id);
Copy link
Member

Choose a reason for hiding this comment

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

👍🏻👍🏻

setTodoList(updatedTodoList);
}

return (
<li className="li" css={liStyle}>
<input
type="checkbox"
className="check-box"
checked={todoItem.checked}
onChange={onChangeCheckbox}
css={checkBoxStyle}
/>
<span
className="span"
// 완료 여부에 따라 스타일 변경
css={todoItem.checked ? spanCheckedStyle : spanStyle}
Copy link
Member

Choose a reason for hiding this comment

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

동적 스타일링까지 아주 좋습니다!!

>
{todoItem.text}
</span>
<button
type="button"
className="delete-button"
onClick={() => onDelete(todoItem.id)}
css={deleteButtonStyle}
>
✖️
</button>
</li>
);
}

ToDoItem.propTypes = {
todoItem: PropTypes.shape({
id: PropTypes.number,
text: PropTypes.string.isRequired,
checked: PropTypes.bool.isRequired,
}),
todoList: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
checked: PropTypes.bool.isRequired,
})
),
setTodoList: PropTypes.func.isRequired,
};
Loading