diff --git a/eslint.config.js b/eslint.config.js index 238d2e4..a4caa38 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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'] }, @@ -35,4 +35,9 @@ export default [ ], }, }, -] + { + rules: { + 'react/no-unknown-property': ['error', { ignore: ['css'] }], + }, + }, +]; diff --git a/package.json b/package.json index 9f1bcb2..055524b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.13.3", + "normalize.css": "^8.0.1", + "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/src/App.jsx b/src/App.jsx index 852a9fe..6e53059 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,85 @@ +/** @jsxImportSource @emotion/react */ +import { css } from '@emotion/react'; +import 'normalize.css'; +import { useEffect, useState } from 'react'; +import InputBox from './components/InputBox.jsx'; +import ToDoItemList from './components/ToDoItemList.jsx'; + function App() { - return
React Todo
; + 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) + ); + `; + + 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/noonfonts_11-01@1.0/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) : []; + }); + + // todoList가 업데이트될 때마다 localStorage에 저장 + useEffect(() => { + localStorage.setItem('todoList', JSON.stringify(todoList)); + }, [todoList]); + + return ( +
+
+

+ To Do List +

+ {/* 할 일 입력 */} + + + {/* 할 일 목록 */} + !item.checked).length + })`} // 아직 완료되지 않은목록의 길이 + todoList={todoList} + setTodoList={setTodoList} + checkedList={false} + /> + + {/* 완료된 목록 */} + item.checked).length})`} // 완료된 목록의 길이 + todoList={todoList} + setTodoList={setTodoList} + checkedList={true} + /> +
+
+ ); } export default App; diff --git a/src/components/InputBox.jsx b/src/components/InputBox.jsx new file mode 100644 index 0000000..b0e048f --- /dev/null +++ b/src/components/InputBox.jsx @@ -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/noonfonts_11-01@1.0/LINESeedKR-Rg.woff2') + format('woff2'); + font-weight: 400; + font-style: normal; + } + 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); + + // form 제출 시 새로고침 방지 + const formClickEvent = (e) => { + e.preventDefault(); + }; + + // input 값 가져오기 + function onChangeInput(e) { + setText(e.target.value); + // e.target에 있는 으로부터 value 값을 가져옴 + } + + // + 버튼 클릭(form 제출) + function onClickButton() { + // 공백 입력 방지 + if (text.trim() === '') return; + + // todoItemList에 값 추가 + const AddTodoList = todoList.concat({ + id: todoList.length, + text, + checked: false, + }); + setTodoList(AddTodoList); + + setText(''); // input 값 초기화 + inputRef.current.focus(); // 버튼 누른 후에도 input box에 자동 포커싱 + } + + return ( +
+
+ + +
+
+ ); +} + +// props 값 검증 +InputBox.propTypes = { + todoList: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + text: PropTypes.string.isRequired, + }).isRequired + ), + setTodoList: PropTypes.func.isRequired, +}; diff --git a/src/components/ToDoItem.jsx b/src/components/ToDoItem.jsx new file mode 100644 index 0000000..3a59905 --- /dev/null +++ b/src/components/ToDoItem.jsx @@ -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/noonfonts_11-01@1.0/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/noonfonts_11-01@1.0/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` + 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); + setTodoList(updatedTodoList); + } + + return ( +
  • + + + {todoItem.text} + + +
  • + ); +} + +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, +}; diff --git a/src/components/ToDoItemList.jsx b/src/components/ToDoItemList.jsx new file mode 100644 index 0000000..24f019c --- /dev/null +++ b/src/components/ToDoItemList.jsx @@ -0,0 +1,99 @@ +/** @jsxImportSource @emotion/react */ +import { css } from '@emotion/react'; +import 'normalize.css'; +import ToDoItem from './ToDoItem'; +import PropTypes from 'prop-types'; + +export default function ToDoItemList({ + title, + todoList, + setTodoList, + checkedList, +}) { + const listBoxStyle = css` + overflow-y: auto; + &::-webkit-scrollbar { + // 스크롤바 모양 변경 + width: 4px; + } + &::-webkit-scrollbar-thumb { + border-radius: 2px; // 스크롤바 모서리 둥글게 + background: rgb(255, 238, 245); // 스크롤바 색상 변경 + } + `; + + const titleStyle = css` + width: 100%; + height: 30px; + border-top: 1px solid; + border-color: rgb(247, 196, 218); + padding-top: 15px; + margin-top: 20px; + @font-face { + font-family: 'LINESeedKR-Bd'; + src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_11-01@1.0/LINESeedKR-Bd.woff2') + format('woff2'); + font-weight: 700; + font-style: normal; + } + font-family: 'LINESeedKR-Bd'; + font-size: 16px; + color: rgb(59, 56, 56); + `; + + const listStyle = css` + width: 100%; + height: 180px; + `; + + const ulStyle = css` + list-style: none; + display: flex; + flex-direction: column; + justify-content: center; + padding-left: 10px; + margin: 0; + `; + + return ( + <> +
    + {title} +
    +
    +
    +
      + {/* todoList에 값이 있을 경우에만 실행 */} + {todoList && + todoList.map((todoItem) => { + // checkedList 값에 따라 'TO DO 목록' 또는 'DONE 목록' 출력 + if (checkedList !== todoItem.checked) return null; + return ( + // 각각의 todoItem 출력 + + ); + })} +
    +
    +
    + + ); +} + +ToDoItemList.propTypes = { + title: PropTypes.string.isRequired, + todoList: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + text: PropTypes.string.isRequired, + checked: PropTypes.bool.isRequired, + }) + ), + setTodoList: PropTypes.func.isRequired, + checkedList: PropTypes.bool.isRequired, +}; diff --git a/src/main.jsx b/src/main.jsx index 3d9da8a..224ec06 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,9 +1,9 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App.jsx' +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.jsx'; createRoot(document.getElementById('root')).render( - , -) + +);