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

Feat: todolist mvc #15

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
75 changes: 49 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,51 @@
# 42gg 프론트엔드 온보딩 1단계

## 공통 조건

- 온보딩 프로젝트는 개인 계정으로 fork하여 진행하고 PR로 제출합니다.
- git / github / code 컨벤션은 42gg notion에 있는 자료를 적극 반영합니다.
- 기본 기능 외 추가 기능, 디자인 구현은 자유입니다.
- 최종 제출품에는 README 작성이 되있어야 합니다.([예시](https://github.com/42organization/42gg.client/blob/main/README.md))

## todo list 만들기

- (필수) Javascript, HTML, CSS
- (필수) todo 생성(Create), 조회(Read) 기능 구현하기 (새로고침 고려 X)
- (필수) todo 완료 상태 체크 기능 구현하기 (정렬은 선택사항)
- (필수) todo 수정(Update), 삭제(Delete) 기능 구현하기 (새로고침 고려 X)
- (선택) 디자인 적용하기
- (선택) 무료로 배포하기

## 참고

- 데이터 관리는 하단의 방식 중 하나 선택하시면 됩니다.
- localstorage
- local server(예. [https://github.com/shal0mdave/todo-api.git](https://github.com/shal0mdave/todo-api.git), lowdb)
- mock api(예. [https://dummyjson.com/](https://dummyjson.com/))
- todo list를 구현하기 위해 필요한 기능들을 미리 생각(그려보고)해보고, 구현해보세요.
- 궁금한 사항은 issue에 등록해주세요.
- yoouyeon 님 배포 페이지| https://verysimpletodolist.netlify.app/
- github io 배포방법 | https://velog.io/@mangojang/github-pages-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0
## 구현 목표
- MVC 패턴을 적용한 TODO-list 만들기
- 함수 재사용성 높이기(실패)
- 로직 명확하게 분리하기(부분적 성공)

### eventtype
```js
const eventType = {
addItem, //아이템 생성*
deleteItem, // 아이템 제거*
setItemStatus, //아이템 상태 변경
editItemMode, // 아이템 내용 변경 가능상태
editItemSave, //아이템 내용 변경 저장
switchView, //화면보여주기 - all / active / disable
dragItemMode, //아이템 드래그를 통한 위치 변경 상태
dragItemSave,
}
```

## MVC 패턴에 따른 전체 구조
컨트롤러, 뷰, 모델로 구분하여 각자 클래스를 만들었습니다.
컨트롤러 클래스에서 전체 렌더/ 조정을 담당합니다.

### 1. 객체 초기화
#### 모델
생성 시 로컬스토리지 값을 확인하고 가져옵니다.
#### 뷰
생성 시 현재 보여줄 item status만 가지고 있습니다.
#### 컨트롤러
만들어진 모델, 뷰 객체를 받아 초기화합니다.
- 값이 없을 경우 "default" 값을 넣어줍니다.
- 뷰 객체로 함수를 넘기고, 뷰 객체는 이를 받아 이벤트리스너에 콜백으로 등록합니다(바인딩)
이때 감싸준 함수는 객체 클로저를 만들어, this가 컨트롤러 객체에 바인딩된 상태를 유지시킵니다.
```js
this.view.bindEvent("deleteBtn", (event) => this.deleteItem(event));
this.view.bindEvent("deleteBtn", this.deleteItem);
```
### 2. 렌더
기본 index.html에 노드구조가 들어가 있습니다.

컨트롤러 생성자는 이벤트리스너를 등록한 후 마지막으로
모델 객체를 불러와 `getItemsByStatus`
뷰에 아이템을 토스합니다. `updateItems`

- 모든 아이템 토글 이벤트는 전체 삭제 후 재등록으로 이뤄집니다.
따로 diff 로직을 넣는 등의 효율은 추구하지 않았습니다...

- 아이템 상태 변경/삭제 시에는 키값을 통해 추적합니다.

68 changes: 68 additions & 0 deletions index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@import url('https://fonts.googleapis.com/css2?family=Diphylleia&display=swap');

.diphylleia-regular {
font-family: "Diphylleia", serif;
font-weight: 400;
font-style: normal;
}

body {
width: 100vh;
height: 100vh;
align-items: center;
display:flex;
font-family: "Diphylleia", serif;
justify-content: center;
background: linear-gradient(180deg, rgba(55,54,54,1) 15%, rgba(189,189,189,1) , rgba(222,222,222,1));
}
h1 {
font-family: "Diphylleia", serif;
color: white;
text-shadow: 1px 1px 5px salmon, 0px 0px 30px rgba(255, 255, 50, 0.5);

}
button {
width: max(30px, max-content);
height: max(30px, max-content);
font-size: small;
}

#app {
width: 100%;
height: 100%;
align-items: center;
/* justify-content: center; */
display: flex;
flex-direction: column;
}

.container{
width: 600px;
height: fit-content;
padding: 10px 20px;
justify-content: center;
align-items: center;
}

#todo-input-container {
border: 2px solid salmon;
}
#todo-list-container {
border:2px solid sandybrown;
}
#todo-input {
width: 100%;
height: 100%;
background-color: rgba(0, 0,0, 0);
box-shadow: none;
}

.todo-item{
width: 100%;
margin: 5px;
border: 0 0 0 1px solid blue;
height: 20px;
display: flex;
align-items: center;
gap: 20px;
}
26 changes: 26 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="index.css" />
<title>TODO~~</title>
</head>
<body>
<div id="app">
<h1>To-do-list</h1>
<form id="todo-input-container" class="container">
<input id="todo-input" />
</form>
<div id="state-container" class="container">
<button class="state" key="all">All</button>
<button class="state" key="active">active</button>
<button class="state" key="complete">complete</button>
</div>
<div id="todo-list-container" class="container">
<ul id="todo-list-ul"></ul>
</div>
</div>
<script src="src/app.js" type="module"></script>
</body>
</html>
48 changes: 48 additions & 0 deletions src/Model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export default class Model {
data;
length;
constructor() {
var strData = [];
try {
strData = JSON.parse(localStorage.getItem("data"))
}
catch (e) {
console.log("data is not valid, inited to default")
}
this.data = strData && strData.length ? strData : [];
}
_updateStorage(){
localStorage.setItem("data", JSON.stringify(this.data));
}
addItem(name) {
const newData =
{
name : name,
isComplete: false,
key : this.data.length,
} // updatedata
this.data.push(newData);
this._updateStorage();
return this.data[this.data.length - 1];
}
deleteItem(key) {
if (this.data.find(item => item.key.toString() === key)){
delete this.data[key];
}
else
console.error("cannot delete Item");
this._updateStorage();
}
getItemByStatus(status){ // assume type comes in correctly
if (status === "all") return this.data;
status = status === "active" ? false : true;
return (this.data.filter((item) => item && item.isComplete === status))
}
updateItemStatus (key, status) {
this.data.forEach((i) => {
if (i && i.key && i.key.toString() === key)
i.isComplete = status;
} );
this._updateStorage();
}
}
77 changes: 77 additions & 0 deletions src/View.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { qs, dc } from "./utils.js"

export default class View {
constructor() {
this.input_container = qs("#todo-input-container");
this.input = qs("#todo-input");
this.list = qs("#todo-list-ul");
this.state_container = qs("#state-container")
this.toggle_all = qs("#state-all")
this.toggle_active = qs("#state-active")
this.toggle_complete = qs("#state-complete")

}
getItem(){
var temp = this.input.value;
this.input.value = "";
return temp;
}
newItem(data) {
const li = dc("li");
li.setAttribute("class", "todo-item");
li.setAttribute("class", data.status ? "active" : "complete");
li.setAttribute("key", data.key);
li.innerHTML = `
<p>${data.name}</p>
<button class="todo-item-delete" key="${data.key}">X</button>
`
const chk = dc("input");
chk.setAttribute("type", "checkbox");
chk.setAttribute("class", "todo-item-complete");
chk.checked = data.status;
li.appendChild(chk);
return (li);
}
updateItem(status, data , key){
if (status === "add"){ //add{
this.list.appendChild(this.newItem(data));
}
else if (status === "delete"){
this.list.removeChild(data);
}
else if (status === "toggle"){
const arr = Array.from(this.list.children);
arr.forEach(item => {
this.list.removeChild(item)
});
data.forEach(item => {
this.list.appendChild(this.newItem(item));
})
}
}
changeItemStatus(item, status, show){
const removeClass = status ? "complete" : "active";
const addClass = status ? "active" : "complete";
item.classList.remove(removeClass);
item.classList.add(addClass);
}
bindEvent(type, callback, param){ //일종의 데이터 초기화
// type: 이벤트 타입
switch (type) {
case "submitInput" :
this.input_container.addEventListener("submit", (e) => {
e.preventDefault();
callback(this.getItem().trim())}
);
break;
case "deleteBtn" :
this.list.addEventListener("click", callback);
break;
case "toggleByStatus" :
this.state_container.addEventListener("click", callback);
break ;
case "stateCheckBtn" :
this.list.addEventListener("click", callback)
}
}
}
10 changes: 10 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Controller from "./controller.js"
import Model from "./Model.js"
import View from "./View.js"

const _Model = new Model();
const _View = new View();
const Todo = new Controller ( _View, _Model);

console.log("loaded");

53 changes: 53 additions & 0 deletions src/controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export default class Controller{
view;
model;
constructor( View, Model ) {
this.view = View,
this.model = Model,
this.status = "active"
this.view.bindEvent("submitInput", (value) => this.addItem(value));
this.view.bindEvent("deleteBtn", (event) => this.deleteItem(event));
this.view.bindEvent("toggleByStatus", (event) => this.toggleByStatus(event));
this.view.bindEvent("stateCheckBtn", (event) => this.changeStatus(event));
if (!this.model.getItemByStatus("all").length)
this.addItem("default");
this.view.updateItem("toggle", this.model.getItemByStatus(this.status));
}
addItem (value) {
const newItem = this.model.addItem(value);
if (this.status !== "compelete")
this.view.updateItem("add", newItem);
}
deleteItem(event) {
if (event.target.className !== "todo-item-delete") return ;
const Item = event.target.closest("li");
this.view.updateItem("delete", Item);
this.model.deleteItem(Item.getAttribute("key"));
}
toggleByStatus(e) {
this.status = e.target.getAttribute("key");
let data = this.model.getItemByStatus(this.status);
this.view.updateItem("toggle", data, this.status);
}
changeStatus(e) {
if (e.target.className !== "todo-item-complete") return;
let item = e.target.closest("li");
let status = e.target.checked;
this.model.updateItemStatus(item.getAttribute("key"), status);
if (this.isMatchStatus(status)){
this.view.changeItemStatus(item, status);
}
else (this.view.updateItem("delete", item))
}
isMatchStatus(status){
if (this.status === "all" || (this.status === "active" && !status ) || (this.status === "active" && status)) return true;
return false;
}
editItemModeOn(e){
if (e.target.className !== "todo-item") return;
this.editItem = e.target;

e.parentNode.replaceChild("")
}
}

14 changes: 14 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const qs = (selector, range) => {
return (range || document).querySelector(selector);
}

export const qsa = (selector, range) => {
return (range || document).querySelectorAll(selector);
}

export const dc = (selector) => {
return document.createElement(selector);
}
export const dct = (value) => {
return document.createTextNode(value);
}