diff --git a/README.md b/README.md index 3c0710d2..d71614da 100644 --- a/README.md +++ b/README.md @@ -1 +1,36 @@ -# react-todo-list-precourse \ No newline at end of file +# πŸ“– ν•  일 λͺ©λ‘ - λ―Έλ‹ˆκ³Όμ œ + +## 과제 μ§„ν–‰ μš”κ΅¬ 사항 βœ… + +- [x] λ―Έμ…˜μ€ ν•  일 λͺ©λ‘ μ €μž₯μ†Œλ₯Ό ν¬ν¬ν•˜κ³  ν΄λ‘ ν•˜λŠ” κ²ƒμœΌλ‘œ μ‹œμž‘ν•œλ‹€. +- [x] κΈ°λŠ₯을 κ΅¬ν˜„ν•˜κΈ° μ „ README.md 에 κ΅¬ν˜„ν•  κΈ°λŠ₯ λͺ©λ‘μ„ 정리해 μΆ”κ°€ν•œλ‹€. +- [x] Git의 컀밋 λ‹¨μœ„λŠ” μ•ž λ‹¨κ³„μ—μ„œ README.md 에 μ •λ¦¬ν•œ κΈ°λŠ₯ λͺ©λ‘ λ‹¨μœ„λ‘œ μΆ”κ°€ν•œλ‹€. + +## πŸš€ κΈ°λŠ₯ μš”κ΅¬ 사항 βœ… + +ν•˜λ£¨ λ˜λŠ” ν•œ 주의 ν•  일 λͺ©λ‘μ„ μ—…λ°μ΄νŠΈν•˜λŠ” ν•  일 λͺ©λ‘μ„ κ΅¬ν˜„ν•œλ‹€. **React 라이브러리λ₯Ό μ‚¬μš©ν•˜μ—¬ μ›Ή μ•±μœΌλ‘œ κ΅¬ν˜„ν•œλ‹€.** + +- [x] ν•  일을 μΆ”κ°€ν•˜κ³  μ‚­μ œν•  수 μžˆλ‹€. +- [x] ν•  일을 μΆ”κ°€ν•  λ•Œ μ‚¬μš©μžλŠ” Enter ν‚€λ‚˜ μΆ”κ°€ λ²„νŠΌμ„ μ‚¬μš©ν•˜μ—¬ ν•  일을 λͺ©λ‘μ— μΆ”κ°€ν•  수 μžˆμ–΄μ•Ό ν•œλ‹€. +- [x] μ‚¬μš©μžκ°€ 아무것도 μž…λ ₯ν•˜μ§€ μ•Šμ€ κ²½μš°μ—λŠ” ν•  일을 μΆ”κ°€ν•  수 μ—†λ‹€. +- [x] ν•  일의 λͺ©λ‘μ„ λ³Ό 수 μžˆλ‹€. +- [x] ν•  일의 μ™„λ£Œ μƒνƒœλ₯Ό μ „ν™˜ν•  수 μžˆλ‹€. + +## πŸš€ 선택 μš”κ΅¬ 사항 βœ… + +- [ ] ν˜„μž¬ μ§„ν–‰ 쀑인 ν•  일, μ™„λ£Œλœ ν•  일, λͺ¨λ“  ν•  일을 필터링할 수 μžˆλ‹€. +- [x] ν•΄μ•Ό ν•  일의 총개수λ₯Ό 확인할 수 μžˆλ‹€. +- [ ] μƒˆλ‘œκ³ μΉ¨μ„ ν•˜μ—¬λ„ 이전에 μž‘μ„±ν•œ λ°μ΄ν„°λŠ” μœ μ§€λ˜μ–΄μ•Ό ν•œλ‹€. + +## 🎯 주의 ν•΄μ•Ό ν•  ν”„λ‘œκ·Έλž˜λ° μš”κ΅¬ 사항 βœ… + +- [x] ν”„λ‘œκ·Έλž¨ μ‹€ν–‰μ˜ μ‹œμž‘μ μ€ App.js 이닀. +- [x] package.json νŒŒμΌμ€ λ³€κ²½ν•  수 μ—†μœΌλ©°, 제곡된 λΌμ΄λΈŒλŸ¬λ¦¬μ™€ μŠ€νƒ€μΌ 라이브러리 μ΄μ™Έμ˜ μ™ΈλΆ€ λΌμ΄λΈŒλŸ¬λ¦¬λŠ” μ‚¬μš©ν•˜μ§€ μ•Šμ•„μ•Ό ν•œλ‹€. +- [x] ν”„λ‘œκ·Έλž¨ μ’…λ£Œ μ‹œ process.exit() λ₯Ό ν˜ΈμΆœν•˜μ§€ μ•ŠλŠ”λ‹€. +- [x] indent(인덴트, λ“€μ—¬μ“°κΈ°) depthλ₯Ό 3이 λ„˜μ§€ μ•Šλ„λ‘ κ΅¬ν˜„ν•œλ‹€. 2κΉŒμ§€λ§Œ ν—ˆμš©ν•œλ‹€. +- [x] ν•¨μˆ˜(λ˜λŠ” λ©”μ„œλ“œ)의 길이가 15라인을 λ„˜μ–΄κ°€μ§€ μ•Šλ„λ‘ κ΅¬ν˜„ν•œλ‹€. + +### 과제 제좜 μ „ 체크리슀트 + +- [x] ν„°λ―Έλ„μ—μ„œ node --version 을 μ‹€ν–‰ν•˜μ—¬ Node.js 버전이 18.17.1 이상인지 ν™•μΈν•œλ‹€. +- [x] npm install , npm run start λͺ…λ Ή μž…λ ₯ν•˜μ—¬ νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜ν•œ ν›„ μ‹€ν–‰ν•˜λŠ” 데 λ¬Έμ œκ°€ μ—†μ–΄μ•Ό ν•œλ‹€. diff --git a/index.html b/index.html index b021b5c8..351ba609 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,12 @@ - - + + -
- +
+ diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..973947cd --- /dev/null +++ b/src/App.css @@ -0,0 +1,3 @@ +body { + background: #d3d3d3; +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 00000000..2d83c8ec --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,21 @@ +import React from "react"; +import "./App.css"; +import TodoBox from "./components/TodoBox"; +import TodoHead from "./components/TodoHead"; +import TodoList from "./components/TodoList"; +import TodoCreate from "./components/TodoCreate"; +import { TodoProvider } from "./TodoContext"; + +function App() { + return ( + + + + + + + + ); +} + +export default App; diff --git a/src/TodoContext.jsx b/src/TodoContext.jsx new file mode 100644 index 00000000..ea7560f2 --- /dev/null +++ b/src/TodoContext.jsx @@ -0,0 +1,70 @@ +import React, { useReducer, createContext, useContext, useRef } from "react"; + +const initialTodos = [ + { + id: 1, + text: "μ‹€μ‹œκ°„ μ‹œμŠ€ν…œ κ³Όμ œμ™„μ„±", + done: true, + }, + { + id: 2, + text: "μΊ‘μŠ€ν†€ λ””μžμΈ μ΅œμ’… λ³΄κ³ μ„œ μž‘μ„±", + done: true, + }, + { + id: 3, + text: "κ³΅μž‘λ°œ λ°œν‘œ 자료 μ™„μ„±", + done: false, + }, + { + id: 4, + text: "μΉ΄ν…ŒμΊ  λ‚΄μš© 볡슡", + done: false, + }, +]; + +function todoReducer(state, action) { + switch (action.type) { + case "CREATE": + return state.concat(action.todo); + case "TOGGLE": + return state.map((todo) => + todo.id === action.id ? { ...todo, done: !todo.done } : todo + ); + case "REMOVE": + return state.filter((todo) => todo.id !== action.id); + default: + throw new Error(`Unhandled action type: ${action.type}`); + } +} + +const TodoStateContext = createContext(); +const TodoDispatchContext = createContext(); +const TodoNextIdContext = createContext(); + +export function TodoProvider({ children }) { + const [state, dispatch] = useReducer(todoReducer, initialTodos); + const nextId = useRef(5); + + return ( + + + + {children} + + + + ); +} + +export function useTodoState() { + return useContext(TodoStateContext); +} + +export function useTodoDispatch() { + return useContext(TodoDispatchContext); +} + +export function useTodoNextId() { + return useContext(TodoNextIdContext); +} diff --git a/src/components/TodoBox.jsx b/src/components/TodoBox.jsx new file mode 100644 index 00000000..0eebb760 --- /dev/null +++ b/src/components/TodoBox.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import "../styles/TodoBox.css"; + +function TodoBox({ children }) { + return
{children}
; +} + +export default TodoBox; diff --git a/src/components/TodoCreate.jsx b/src/components/TodoCreate.jsx new file mode 100644 index 00000000..2703d810 --- /dev/null +++ b/src/components/TodoCreate.jsx @@ -0,0 +1,57 @@ +import React, { useState } from "react"; +import "../styles/TodoCreate.css"; +import addImage from "../images/add.png"; +import { useTodoDispatch, useTodoNextId } from "../TodoContext"; + +function TodoCreate() { + const [open, setOpen] = useState(false); + const [value, setValue] = useState(""); + const dispatch = useTodoDispatch(); + const nextId = useTodoNextId(); + const onToggle = () => setOpen(!open); + const onChange = (e) => setValue(e.target.value); + const onSubmit = (e) => { + e.preventDefault(); + if (value.trim().length > 0) { + // 곡백 μž…λ ₯ λ°©μ§€ + dispatch({ + type: "CREATE", + todo: { + id: nextId.current, + text: value, + done: false, + }, + }); + setValue(""); + setOpen(false); + nextId.current += 1; + } + }; + + return ( + <> + {open && ( +
+
+ +
+
+ )} + + + ); +} + +export default React.memo(TodoCreate); diff --git a/src/components/TodoHead.jsx b/src/components/TodoHead.jsx new file mode 100644 index 00000000..95bae74c --- /dev/null +++ b/src/components/TodoHead.jsx @@ -0,0 +1,28 @@ +import React from "react"; +import "../styles/TodoHead.css"; +import { useTodoState } from "../TodoContext"; + +function TodoHead() { + const todos = useTodoState(); + const undoneTasks = todos.filter((todo) => !todo.done); + + const today = new Date(); + const dateString = today.toLocaleDateString("ko-KR", { + year: "numeric", + month: "long", + day: "numeric", + }); + const dayName = today.toLocaleDateString("ko-KR", { weekday: "long" }); + + return ( +
+

Todos

+
+ {dateString} {dayName} +
+
남은 일 : {undoneTasks.length}개
+
+ ); +} + +export default TodoHead; diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx new file mode 100644 index 00000000..ad2fbdf3 --- /dev/null +++ b/src/components/TodoItem.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import "../styles/TodoItem.css"; +import doneImage from "../images/done.png"; +import deleteImage from "../images/delete.png"; +import { useTodoDispatch } from "../TodoContext"; + +function TodoItem({ id, done, text }) { + const dispatch = useTodoDispatch(); + const onToggle = () => dispatch({ type: "TOGGLE", id }); + const onRemove = () => dispatch({ type: "REMOVE", id }); + return ( +
+
+ {done && done} +
+
{text}
+
+ Delete +
+
+ ); +} + +export default React.memo(TodoItem); +// λ‹€λ₯Έ ν•­λͺ© μ—…λ°μ΄νŠΈ -> λΆˆν•„μš”ν•œ λ¦¬λ Œλ”λ§ λ°©μ§€λ‘œ μ„±λŠ₯ μ΅œμ ν™” diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx new file mode 100644 index 00000000..cf96d478 --- /dev/null +++ b/src/components/TodoList.jsx @@ -0,0 +1,22 @@ +import React from "react"; +import "../styles/TodoList.css"; +import TodoItem from "./TodoItem"; +import { useTodoState } from "../TodoContext"; + +function TodoList() { + const todos = useTodoState(); + return ( +
+ {todos.map((todo) => ( + + ))} +
+ ); +} + +export default TodoList; diff --git a/src/images/add.png b/src/images/add.png new file mode 100644 index 00000000..8ce7c053 Binary files /dev/null and b/src/images/add.png differ diff --git a/src/images/delete.png b/src/images/delete.png new file mode 100644 index 00000000..a564a544 Binary files /dev/null and b/src/images/delete.png differ diff --git a/src/images/done.png b/src/images/done.png new file mode 100644 index 00000000..bdd61728 Binary files /dev/null and b/src/images/done.png differ diff --git a/src/main.js b/src/main.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 00000000..8db5acb8 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; + +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render( + + + +); diff --git a/src/styles/TodoBox.css b/src/styles/TodoBox.css new file mode 100644 index 00000000..202d17ad --- /dev/null +++ b/src/styles/TodoBox.css @@ -0,0 +1,13 @@ +.todo-box { + width: 512px; + height: 768px; + position: relative; + background: white; + border-radius: 16px; + box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.04); + margin: 0 auto; + margin-top: 96px; + margin-bottom: 32px; + display: flex; + flex-direction: column; +} diff --git a/src/styles/TodoCreate.css b/src/styles/TodoCreate.css new file mode 100644 index 00000000..4d8b1d25 --- /dev/null +++ b/src/styles/TodoCreate.css @@ -0,0 +1,75 @@ +.circleButton { + background: #38d9a9; + z-index: 5; + cursor: pointer; + width: 80px; + height: 80px; + display: block; + align-items: center; + justify-content: center; + font-size: 60px; + position: absolute; + left: 50%; + bottom: 0px; + transform: translate(-50%, 50%); + color: white; + border-radius: 50%; + border: none; + outline: none; + display: flex; + align-items: center; + justify-content: center; + transition: 0.125s all ease-in; +} + +.circleButton:hover { + background: #63e6be; +} + +.circleButton:active { + background: #20c997; +} + +.circleButton.open { + background: #ff6b6b; +} + +.circleButton.open:hover { + background: #ff8787; +} + +.circleButton.open:active { + background: #fa5252; +} + +.circleButton.open { + transform: translate(-50%, 50%) rotate(45deg); +} + +.Input { + padding: 12px; + border-radius: 4px; + border: 1px solid rgb(254, 254, 254); + width: 100%; + outline: none; + font-size: 18px; + box-sizing: border-box; +} + +.InsertFormPositioner { + width: 100%; + bottom: 0; + left: 0; + position: absolute; +} + +.InsertForm { + background: #f8f9fa; + padding-left: 32px; + padding-top: 32px; + padding-right: 32px; + padding-bottom: 72px; + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; + border-top: 1px solid #e9ecef; +} diff --git a/src/styles/TodoHead.css b/src/styles/TodoHead.css new file mode 100644 index 00000000..6c2e19e8 --- /dev/null +++ b/src/styles/TodoHead.css @@ -0,0 +1,26 @@ +.todo-head-block { + padding-top: 48px; + padding-left: 32px; + padding-right: 32px; + padding-bottom: 24px; + border-bottom: 1px solid #e9ecef; +} + +.todo-head-block h1 { + margin: 0; + font-size: 36px; + color: #343a40; +} + +.todo-head-block .day { + margin-top: 4px; + color: #868e96; + font-size: 21px; +} + +.todo-head-block .tasks-left { + color: #20c997; + font-size: 18px; + margin-top: 40px; + font-weight: bold; +} diff --git a/src/styles/TodoItem.css b/src/styles/TodoItem.css new file mode 100644 index 00000000..00a4570b --- /dev/null +++ b/src/styles/TodoItem.css @@ -0,0 +1,53 @@ +.remove { + display: flex; + align-items: center; + justify-content: center; + color: #dee2e6; + font-size: 24px; + cursor: pointer; + display: none; +} + +.todo-item-block { + display: flex; + align-items: center; + padding-top: 12px; + padding-bottom: 12px; +} + +.todo-item-block:hover .remove { + display: initial; +} + +.check-circle { + width: 32px; + height: 32px; + border-radius: 16px; + border: 1px solid #ced4da; + font-size: 24px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 20px; + cursor: pointer; +} + +.check-circle.done { + border: 1px solid #38d9a9; + color: #38d9a9; +} + +.text { + flex: 1; + font-size: 21px; + color: #495057; +} + +.text.done { + color: #ced4da; +} + +img { + width: 24px; + height: 24px; +} diff --git a/src/styles/TodoList.css b/src/styles/TodoList.css new file mode 100644 index 00000000..455f4a16 --- /dev/null +++ b/src/styles/TodoList.css @@ -0,0 +1,6 @@ +.todo-list { + flex: 1; + padding: 20px 32px; + padding-bottom: 48px; + overflow-y: auto; +}