Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,36 @@
# react-todo-list-precourse
# 📖 할 일 목록 - 미니과제

## 과제 진행 요구 사항 ✅

- [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 명령 입력하여 패키지를 설치한 후 실행하는 데 문제가 없어야 한다.
8 changes: 4 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<!doctype html>
<html lang="en">
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<div id="root"></div>
<script type="module" src="src/main.jsx"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
background: #d3d3d3;
}
21 changes: 21 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<TodoProvider>
<TodoBox>
<TodoHead />
<TodoList />
<TodoCreate />
</TodoBox>
</TodoProvider>
);
}

export default App;
70 changes: 70 additions & 0 deletions src/TodoContext.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<TodoStateContext.Provider value={state}>
<TodoDispatchContext.Provider value={dispatch}>
<TodoNextIdContext.Provider value={nextId}>
{children}
</TodoNextIdContext.Provider>
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
);
}

export function useTodoState() {
return useContext(TodoStateContext);
}

export function useTodoDispatch() {
return useContext(TodoDispatchContext);
}

export function useTodoNextId() {
return useContext(TodoNextIdContext);
}
8 changes: 8 additions & 0 deletions src/components/TodoBox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from "react";
import "../styles/TodoBox.css";

function TodoBox({ children }) {
return <div className="todo-box">{children}</div>;
}

export default TodoBox;
57 changes: 57 additions & 0 deletions src/components/TodoCreate.jsx
Original file line number Diff line number Diff line change
@@ -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 && (
<div className="InsertFormPositioner">
<form className="InsertForm" onSubmit={onSubmit}>
<input
className="Input"
autoFocus
placeholder="할 일을 입력 후, Enter 를 누르세요"
onChange={onChange}
value={value}
/>
</form>
</div>
)}
<button
className={`circleButton ${open ? "open" : ""}`}
onClick={onToggle}
open={open}
>
<img src={addImage} alt="Add" style={{ width: "80%", height: "80%" }} />
</button>
</>
);
}

export default React.memo(TodoCreate);
28 changes: 28 additions & 0 deletions src/components/TodoHead.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="todo-head-block">
<h1>Todos</h1>
<div className="day">
{dateString} {dayName}
</div>
<div className="tasks-left">남은 일 : {undoneTasks.length}개</div>
</div>
);
}

export default TodoHead;
25 changes: 25 additions & 0 deletions src/components/TodoItem.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="todo-item-block">
<div className={`check-circle`} onClick={onToggle}>
{done && <img src={doneImage} alt="done" />}
</div>
<div className={`text ${done ? "done" : ""}`}>{text}</div>
<div className="remove" onClick={onRemove}>
<img src={deleteImage} alt="Delete" />
</div>
</div>
);
}

export default React.memo(TodoItem);
// 다른 항목 업데이트 -> 불필요한 리렌더링 방지로 성능 최적화
22 changes: 22 additions & 0 deletions src/components/TodoList.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="todo-list">
{todos.map((todo) => (
<TodoItem
key={todo.id}
id={todo.id}
text={todo.text}
done={todo.done}
/>
))}
</div>
);
}

export default TodoList;
Binary file added src/images/add.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/images/delete.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/images/done.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file removed src/main.js
Empty file.
10 changes: 10 additions & 0 deletions src/main.jsx
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
);
13 changes: 13 additions & 0 deletions src/styles/TodoBox.css
Original file line number Diff line number Diff line change
@@ -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;
}
75 changes: 75 additions & 0 deletions src/styles/TodoCreate.css
Original file line number Diff line number Diff line change
@@ -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;
}
Loading