-
Notifications
You must be signed in to change notification settings - Fork 29
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
[Mission5/김혜성] Project_Notion_VanillaJs 과제 #55
base: 4/#5_hyesung99
Are you sure you want to change the base?
Changes from 68 commits
ee84ef9
38d8c7b
d9f3cf6
393cef6
133a1e9
1d6b5d5
838f401
af01e93
52393ce
116b5ed
4a0c24f
2ac83d1
f7e64ab
29c7cd0
eae1049
156c110
6351098
46c9897
d246857
2eac3d3
7826a94
9e10231
827ed4e
957141d
8df24f9
2fc30e4
9672318
604902c
9d14aac
196ab68
36fb2e3
4c2a5cf
239d455
ef56aea
8e99406
7d25bbb
4090708
611bb28
a2df18e
e3cd4e4
bdd1cae
283036b
6dd26e3
2f7a054
8655de9
0841f94
336df26
fdd8074
ce0fe3e
ff32c7d
5d78335
d3d0f14
7c2e953
5fa46b1
9cfaa2c
5d3d68b
c51f757
3ba26b4
957acc8
096f191
1b4b093
17ce2b7
ab72ead
69d59c3
ec5c578
680dcdf
3cbdf6f
723b50e
69e2618
ba0e86c
33d0686
fc42542
9de70ed
1f7a2c2
024a321
6d8dbff
6b10fd5
9dcec5f
83c8604
9ff8141
3c71d19
4fa0b55
ee0342f
81ca0ae
f0889b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<!DOCTYPE html> | ||
<html lang="ko"> | ||
<head> | ||
<title>Notion Clone Site</title> | ||
<link rel="stylesheet" href="/src/style/style.css" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<meta | ||
name="description" | ||
content="This is a text editor made with vanilla JavaScript for learning SPA methods and components. | ||
" | ||
/> | ||
<meta name="keyword" content="HTML, meta, tag, element, reference" /> | ||
<meta charset="UTF-8" /> | ||
<meta name="author" content="sungbird" /> | ||
</head> | ||
<body> | ||
<main id="app"></main> | ||
<script src="/src/main.js" type="module"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import Component from "./core/Component.js"; | ||
import { DocumentTreeComponent, EditorComponent } from "./Component/index.js"; | ||
import { getDocumentTree } from "./service/index.js"; | ||
import { | ||
addDocumentButtonClickEvnet, | ||
documentLinkClickEvent, | ||
deleteDocumentButtonClickEvent, | ||
documentInputChangeEvent, | ||
textareaKeyupEvent, | ||
titleKeyupEvent, | ||
titleFocusoutEvent, | ||
textareaFocusoutEvent, | ||
} from "./events/index.js"; | ||
import { Document } from "./domain/index.js"; | ||
import { initDocument } from "./constants.js/constants.js"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 불러오는 모습이 조금 요상하네요..!? |
||
|
||
export default class App extends Component { | ||
async render() { | ||
this.$target.innerHTML = ` | ||
<aside id='documentTree'></aside> | ||
<section id='editor'></section> | ||
`; | ||
|
||
const $documentTree = this.$target.querySelector("#documentTree"); | ||
const $editor = this.$target.querySelector("#editor"); | ||
this.documentTree = new DocumentTreeComponent({ | ||
$target: $documentTree, | ||
initialState: await getDocumentTree(), | ||
events: [ | ||
{ | ||
action: "click", | ||
tag: "a", | ||
target: "a", | ||
callback: ({ target, event }) => | ||
documentLinkClickEvent({ | ||
event, | ||
app: this, | ||
component: this.editor, | ||
url: target.href, | ||
}), | ||
}, | ||
{ | ||
action: "click", | ||
tag: ".addDocumentButton", | ||
target: "li", | ||
callback: ({ event, target }) => | ||
addDocumentButtonClickEvnet({ | ||
event, | ||
target, | ||
documentTree: this.documentTree, | ||
}), | ||
}, | ||
{ | ||
action: "click", | ||
tag: ".deleteDocumentButton", | ||
target: "li", | ||
callback: async ({ target }) => | ||
deleteDocumentButtonClickEvent({ | ||
documentTree: this.documentTree, | ||
editor: this.editor, | ||
target, | ||
}), | ||
}, | ||
{ | ||
action: "change", | ||
tag: "input", | ||
target: "li", | ||
callback: async ({ event, target }) => | ||
documentInputChangeEvent({ | ||
event, | ||
documentTree: this.documentTree, | ||
target, | ||
}), | ||
}, | ||
], | ||
}); | ||
|
||
this.editor = new EditorComponent({ | ||
$target: $editor, | ||
initialState: new Document(initDocument), | ||
events: [ | ||
{ | ||
action: "keyup", | ||
tag: ".textarea", | ||
target: ".textarea", | ||
callback: ({ target }) => { | ||
textareaKeyupEvent({ | ||
title: this.editor.state.title, | ||
content: target.innerHTML, | ||
}); | ||
}, | ||
}, | ||
{ | ||
action: "keyup", | ||
tag: ".title", | ||
target: ".title", | ||
callback: ({ target }) => { | ||
titleKeyupEvent({ | ||
title: target.innerHTML, | ||
content: this.state.content, | ||
}); | ||
}, | ||
}, | ||
{ | ||
action: "focusout", | ||
tag: ".title", | ||
target: ".title", | ||
callback: async ({ target }) => { | ||
titleFocusoutEvent({ | ||
documentTree: this.documentTree, | ||
editor: this.editor, | ||
title: target.innerHTML, | ||
}); | ||
}, | ||
}, | ||
{ | ||
action: "focusout", | ||
tag: ".textarea", | ||
target: ".textarea", | ||
callback: ({ target }) => { | ||
textareaFocusoutEvent({ | ||
editor: this.editor, | ||
content: target.innerHTML, | ||
}); | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import Component from "../core/Component.js"; | ||
import { DocumentTreeBranch } from "../domain/index.js"; | ||
import { DocumentTreeBranchComponent } from "./index.js"; | ||
|
||
export default class DocumentTreeComponent extends Component { | ||
render() { | ||
this.$target.innerHTML = ` | ||
<div class="rootUl"> | ||
</div> | ||
<button class="addRootDocumentButton addDocumentButton">+</button> | ||
`; | ||
|
||
const $rootUl = this.$target.querySelector(".rootUl"); | ||
this.state.documentTree.forEach((doc) => { | ||
this.getTemplate({ | ||
$target: $rootUl, | ||
doc, | ||
}); | ||
}); | ||
} | ||
|
||
getTemplate({ $target, doc }) { | ||
new DocumentTreeBranchComponent({ | ||
$target, | ||
initialState: new DocumentTreeBranch(doc), | ||
events: [], | ||
}); | ||
|
||
if (doc.documents.length > 0) { | ||
const $ul = document.createElement("ul"); | ||
$target.appendChild($ul); | ||
doc.documents.forEach((childDoc) => { | ||
this.getTemplate({ $target: $ul, doc: childDoc }); | ||
}); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import Component from "../core/Component.js"; | ||
|
||
export default class DocumentTreeBranchComponent extends Component { | ||
render() { | ||
const $li = document.createElement("li"); | ||
$li.id = this.state.id; | ||
$li.innerHTML = ` | ||
<span class="documentLicontainer"> | ||
<a class="documentLink" href="/documents/${this.state.id}">${this.state.title}</a> | ||
<span class="documentTreeButtonContainer"> | ||
<button class="addDocumentButton" data-id="${this.state.id}">+</button> | ||
<button class="deleteDocumentButton" data-id="${this.state.id}">x</button> | ||
</span> | ||
</span> | ||
`; | ||
this.$target.appendChild($li); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import Component from "../core/Component.js"; | ||
|
||
export default class EditorComponent extends Component { | ||
render() { | ||
this.$target.innerHTML = ` | ||
<div class="editorContainer"> | ||
<div class="title" ${ | ||
this.state.id === -1 | ||
? `contentEditable="false"` | ||
: `contentEditable="true"` | ||
} name="title"> | ||
${this.state.title || "제목을 입력하세요"} | ||
</div> | ||
<div class="textarea" ${ | ||
this.state.id === -1 | ||
? `contentEditable="false"` | ||
: `contentEditable="true"` | ||
} name="textarea"> | ||
${this.state.content || ""} | ||
</div> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 찾아보니 정말 그렇군요... IE에서도 호환이 되지 않으니! id나 class값으로 충분한데 굳이 name을 쓸필요는 없겠네요ㅎㅎ; |
||
</div> | ||
`; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { default as DocumentTreeBranchComponent } from "./DocumentTreeBranch.component.js"; | ||
export { default as DocumentTreeComponent } from "./DocumentTree.component.js"; | ||
export { default as EditorComponent } from "./Editor.component.js"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export const API_END_POINT = "https://kdt-frontend.programmers.co.kr"; | ||
|
||
export const request = async (url, options = {}) => { | ||
const res = await fetch(`${API_END_POINT}${url}`, { | ||
...options, | ||
headers: { | ||
"x-username": "sungbird", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. x-username을 여기서 정의하기보단, 계층을 하나 더 만들어서 관리해주면 어떨까 싶어요! 지금은 이 유저에 종속적인 느낌이라서요 ㅎㅎ 로그인 기능이 들어온다고 생각해보세요! |
||
"Content-Type": "application/json", | ||
}, | ||
Comment on lines
+5
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. options에 headers가 있을 경우에, 여기서 정의한 headers가 덮어씌워지지 않을까요? |
||
}); | ||
|
||
return await res.json(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기서 await을 해주고 안 해주고가 어떤 차이가 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. await을 하지 않으면 promise객체가 반환되고 , await을 한다면 content-type의 header를 json으로 바꾼뒤 요청을 다시 보내 |
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export const initDocument = { | ||
id: -1, | ||
title: "환영합니다!", | ||
content: "문서를 선택해주세요", | ||
documents: [], | ||
createdAt: "000-000", | ||
updatedAt: "000-000", | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 폴더 이름이 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오잉 수정했습니다...ㅋㅋㅠㅠ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 음... 이것도 domain에서 들고있어야 하는 값이 아닐까 싶어요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 변하지 않는 상수값이라고 생각해서 constant에 넣었는데 생각해보니 document에 종속적인 데이터네요! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
export default class Component { | ||
#$target; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
#state; | ||
#events; | ||
#allowedProperties = ["$target", "initialState", "events"]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. static #allowedProperties = ["$target", "initialState", "events"]; 이렇게 static 멤버 변수로 선언하면, Component.#allowedProperties 이렇게 메소드 내부에서 가져와서 사용할 수 있답니다! 이 두 가지가 어떤 차이가 있을까요? |
||
|
||
constructor(properties) { | ||
this.validate(properties); | ||
this.#$target = properties.$target; | ||
this.#state = properties.initialState; | ||
this.#events = properties.events; | ||
this.setEvent(this.#events); | ||
this.render(); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. props 같은건 필요 없을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금 properties로 받고 있는데 이것 자체가 일종의 props가 아닐까? 해서 따로 선언하지 않았습니다...! target과 initalState는 성격이 다르니까 따로 빼고 props를 선언하고 props안에 이벤트를 넣고 이외의 것들도 props에 넣어도 될것 같긴 한데... props안에 events를 넣으면 코드의 depth가 깊어지는것 같기도하고 뭐가 좋은지 잘 모르겠습니다ㅠㅠ |
||
|
||
validate(properties) { | ||
if (typeof properties !== "object") | ||
throw new Error( | ||
`Component: properties는 object 타입이어야 합니다. 현재타입 : ${typeof properties}` | ||
); | ||
Comment on lines
+22
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 배열의 경우에도 object로 나오지 않을까요!? |
||
|
||
Object.keys(properties).forEach((propertie) => { | ||
if (!this.#allowedProperties.includes(propertie)) { | ||
throw new Error( | ||
`Component: ${propertie} 프로퍼티는 필수로 포함되어야 합니다.` | ||
); | ||
} | ||
}); | ||
|
||
const { $target, initialState, events } = properties; | ||
|
||
if (typeof $target !== "object") { | ||
throw new Error( | ||
`Component: $target은 object 타입이어야 합니다. 현재타입 : ${typeof $target}` | ||
); | ||
} | ||
|
||
if (typeof initialState !== "object") { | ||
throw new Error( | ||
`Component: initialState는 object 타입이어야 합니다. 현재타입 : ${typeof initialState}` | ||
); | ||
} | ||
|
||
if (!Array.isArray(events)) { | ||
throw new Error( | ||
`Component: events는 Array 타입이어야 합니다. 현재타입 : ${typeof events}` | ||
); | ||
} | ||
} | ||
|
||
setEvent(events) { | ||
if (events) events.forEach((event) => this.setEventDelegation(event)); | ||
} | ||
|
||
render() {} | ||
|
||
setEventDelegation({ action, tag, target, callback }) { | ||
this.#$target.addEventListener(action, (event) => { | ||
if (event.target.closest(`${tag}`)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기도 동일합니다~! |
||
callback({ event, target: event.target.closest(`${target}`) }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. callback({ event, target: event.target.closest(target) }); 템플릿 리터럴은 없어도 될 것 같네요 ㅋㅋ |
||
} | ||
}); | ||
} | ||
|
||
get state() { | ||
return this.#state; | ||
} | ||
|
||
set state(newState) { | ||
this.#state = newState; | ||
this.render(); | ||
} | ||
|
||
get $target() { | ||
return this.#$target; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
export default class DomainModel { | ||
_properties; | ||
_domainName; | ||
allowedProperties; | ||
JunilHwang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
constructor(properties, domainName) { | ||
this._properties = properties; | ||
this._domainName = domainName; | ||
} | ||
JunilHwang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
clone(newPropertie) { | ||
return new this.constructor( | ||
Object.assign({}, this._properties, newPropertie) | ||
JunilHwang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
); | ||
} | ||
|
||
validate() { | ||
if (typeof this._properties !== "object") { | ||
throw new Error( | ||
`${ | ||
this._domainName | ||
}: properties는 object 타입이어야 합니다. 현재타입 : ${typeof properties}` | ||
); | ||
} | ||
|
||
Object.keys(this._properties).forEach((propertie) => { | ||
if (!Object.keys(this.allowedProperties).includes(propertie)) { | ||
throw new Error( | ||
`${this._domainName}: ${propertie} 프로퍼티는 필수로 포함되어야 합니다.` | ||
); | ||
} | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. property 라고 표현해야 맞지 않을까요!? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제일 중요한 언어는 역시 영어군요 |
||
|
||
Object.keys(this._properties).forEach((propertie) => { | ||
if (this.allowedProperties[propertie] === "array") { | ||
if (!Array.isArray(this._properties[propertie])) { | ||
throw new Error( | ||
`${ | ||
this._domainName | ||
}: ${propertie} 프로퍼티는 array타입이어야 합니다. 현재타입 : ${typeof this | ||
._properties[propertie]}` | ||
); | ||
} | ||
} else if ( | ||
typeof this._properties[propertie] !== this.allowedProperties[propertie] | ||
) { | ||
throw new Error( | ||
`${this._domainName}: ${propertie} 프로퍼티는 ${ | ||
this.allowedProperties[propertie] | ||
} 타입이어야 합니다. 현재타입 : ${typeof this._properties[propertie]}` | ||
); | ||
} | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 구조해제할당을 통해서 this의 사용을 줄여주면 좋지 않을까 싶네요 ㅋㅋ |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
특정 document 페이지에서 새로고침을 하면 path는 그대로고 첫페이지로 이동됩니다!
아마 라우트 기능이 없어서 그런거 같아요!