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}
+
+ onDelete(todoItem.id)}
+ css={deleteButtonStyle}
+ >
+ ✖️
+
+
+ );
+}
+
+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(
- ,
-)
+
+);