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

[Mission5/김혜성] Project_Notion_VanillaJs 과제 #55

Open
wants to merge 85 commits into
base: 4/#5_hyesung99
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
ee84ef9
컴포넌트 추상화
hyesung99 Jun 29, 2023
38d8c7b
refactor : 컴포넌트 멤버변수 private에서 protected로 변경
hyesung99 Jun 29, 2023
d9f3cf6
add : #editor, #documentTree 최초 style적용
hyesung99 Jun 29, 2023
393cef6
add : DocumetnTree, Editor 최초 생성
hyesung99 Jun 29, 2023
133a1e9
add : document 클릭시 pushState
hyesung99 Jun 29, 2023
1d6b5d5
add : editor에 textarea추가
hyesung99 Jun 29, 2023
838f401
add : route() 추가 id기반으로 특정document가져옴 (DUMMY DATA)
hyesung99 Jun 30, 2023
af01e93
add : 임시저장기능 추가
hyesung99 Jun 30, 2023
52393ce
add : request 추가
hyesung99 Jun 30, 2023
116b5ed
add : document tree구조 생성
hyesung99 Jun 30, 2023
4a0c24f
refatcor : a href=document/id 로 수정
hyesung99 Jul 1, 2023
2ac83d1
add : documentTree의 +버튼 누르면 input생성
hyesung99 Jul 3, 2023
f7e64ab
add : +누르고 input입력하면 자식 li생성
hyesung99 Jul 3, 2023
29c7cd0
add : api document연결 / 최초 document tree추가 버튼 생성
hyesung99 Jul 3, 2023
eae1049
add : add createUUID
hyesung99 Jul 3, 2023
156c110
add : 삭제 버튼 추가 / documentTree api연결
hyesung99 Jul 3, 2023
6351098
add : root 추가 버튼 유지
hyesung99 Jul 3, 2023
46c9897
delete : id찾기 삭제
hyesung99 Jul 4, 2023
d246857
delete : id찾기 삭제
hyesung99 Jul 4, 2023
2eac3d3
add : 임시 저장 기능 api연동
hyesung99 Jul 4, 2023
7826a94
add : domain 생성
hyesung99 Jul 4, 2023
9e10231
refactor : DocumentTree validation thorw error로 변경
hyesung99 Jul 5, 2023
827ed4e
fix : DocumentTree initialState설정 오류 수정
hyesung99 Jul 5, 2023
957141d
refactor : DocumentTree 유효성 검사
hyesung99 Jul 5, 2023
8df24f9
add : document domain 생성
hyesung99 Jul 5, 2023
2fc30e4
add : input 위치 변경
hyesung99 Jul 5, 2023
9672318
add : document Tree 스타일 설정
hyesung99 Jul 5, 2023
604902c
add : 제목 추가
hyesung99 Jul 6, 2023
9d14aac
refactor : timeout 수정, timeout시 editor state변경
hyesung99 Jul 6, 2023
196ab68
fix : content undefinded일때 PUT request 오류 수정
hyesung99 Jul 6, 2023
36fb2e3
refactor : Document clone 수정
hyesung99 Jul 6, 2023
4c2a5cf
refactor : editor state 변경하는 시점 keyup에서 focusout으로 수정
hyesung99 Jul 6, 2023
239d455
add : title 수정 가능
hyesung99 Jul 6, 2023
ef56aea
refator : textarea autofocus 해제
hyesung99 Jul 6, 2023
8e99406
add : meta descripton, html lang 설정
hyesung99 Jul 6, 2023
7d25bbb
add : document tree 에
hyesung99 Jul 6, 2023
4090708
add : 임시저장 데이터 불러올떄 updateAt갱신
hyesung99 Jul 6, 2023
611bb28
add : editor 기본 스타일 적용
hyesung99 Jul 6, 2023
a2df18e
add : input 애니메이션 추가
hyesung99 Jul 6, 2023
e3cd4e4
add : documentTree style 추가
hyesung99 Jul 6, 2023
bdd1cae
질문을 위한 커밋입니다.
hyesung99 Jul 6, 2023
283036b
refactor: route 함수 내용 변경 (this 제거)
JunilHwang Jul 6, 2023
6dd26e3
refactor : route 분리
hyesung99 Jul 6, 2023
2f7a054
refactor : addDocumentButtonClickEvent 분리
hyesung99 Jul 6, 2023
8655de9
refactor : service 분리
hyesung99 Jul 6, 2023
0841f94
refactor : saveDocumentToserver호출 시점 route에서 textarea foucsout으로 변경
hyesung99 Jul 6, 2023
336df26
refactor: updateDocumentTree 분리
hyesung99 Jul 6, 2023
fdd8074
refactor : documentStorage 분리
hyesung99 Jul 6, 2023
ce0fe3e
refactor : component props 제거 / component 프로퍼티 private로 변경
hyesung99 Jul 6, 2023
ff32c7d
refactor : editor 이벤트 분리(1)
hyesung99 Jul 6, 2023
5d78335
refactor : editor 이벤트 분리(2)
hyesung99 Jul 6, 2023
d3d0f14
refator : App에 불필요한 파라미터 삭제
hyesung99 Jul 7, 2023
7c2e953
refator : script src 절대경로 변경
hyesung99 Jul 7, 2023
5fa46b1
refactor : Component validation 추가
hyesung99 Jul 8, 2023
9cfaa2c
refactor : DocumentTreeBranch 추가
hyesung99 Jul 8, 2023
5d3d68b
refactor : DocumentTree Link클릭 영역 li전체로 수정
hyesung99 Jul 8, 2023
c51f757
refactor : documentStorage -> documentStorageService
hyesung99 Jul 8, 2023
3ba26b4
refactor : core폴더 생성
hyesung99 Jul 8, 2023
957acc8
refactor : DomainModel 추상화
hyesung99 Jul 8, 2023
096f191
add : DomainModel추상화
hyesung99 Jul 9, 2023
1b4b093
refactor : 각 domain DomainModel상속
hyesung99 Jul 9, 2023
17ce2b7
refator : getDocumentFromStorage default value설정
hyesung99 Jul 9, 2023
ab72ead
fix : DomainModle 프로퍼티 private -> protected변경
hyesung99 Jul 9, 2023
69d59c3
fix : DomainModel clone 오류 수정
hyesung99 Jul 9, 2023
ec5c578
refactor : DomainModel clone 수정
hyesung99 Jul 9, 2023
680dcdf
fix : 임시저장 기능 수정
hyesung99 Jul 9, 2023
3cbdf6f
add : 문서 삭제하면 초기페이지로 이동
hyesung99 Jul 9, 2023
723b50e
fix : 무한 input추가 수정
hyesung99 Jul 10, 2023
69e2618
refactor : domainModel삭제 -> domainService추가
hyesung99 Jul 11, 2023
ba0e86c
add : getHash추가
hyesung99 Jul 11, 2023
33d0686
refactor : document key 상수로 관리
hyesung99 Jul 11, 2023
fc42542
refactor : Component에 props추가
hyesung99 Jul 11, 2023
9de70ed
add : hashrouter생성
hyesung99 Jul 12, 2023
1f7a2c2
refactor : hashRouter로 getDocument, documentLinkClickEvent처리
hyesung99 Jul 12, 2023
024a321
refacotr : mount됐을때 hashRouter observe등록
hyesung99 Jul 12, 2023
6d8dbff
refacotr : getRecentDocument 수정
hyesung99 Jul 12, 2023
6b10fd5
refacotr : cloneDomain 적용
hyesung99 Jul 12, 2023
9dcec5f
refactor : editor의 initalState 변경
hyesung99 Jul 12, 2023
83c8604
fix : cloneDomain 오류 수정
hyesung99 Jul 12, 2023
9ff8141
fix : 임시저장 오류 수정
hyesung99 Jul 12, 2023
3c71d19
fix : css깨짐 현상 수정
hyesung99 Jul 12, 2023
4fa0b55
refactor : propertie array타입 validation
hyesung99 Jul 12, 2023
ee0342f
refactor : apiKey 상수로 관리
hyesung99 Jul 12, 2023
81ca0ae
refactor : iniComponent 상수로 관리
hyesung99 Jul 12, 2023
f0889b2
refactor : updateDocument삭제
hyesung99 Jul 12, 2023
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
20 changes: 20 additions & 0 deletions index.html
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>
130 changes: 130 additions & 0 deletions src/App.js

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

특정 document 페이지에서 새로고침을 하면 path는 그대로고 첫페이지로 이동됩니다!
아마 라우트 기능이 없어서 그런거 같아요!

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";

Choose a reason for hiding this comment

The 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,
});
},
},
],
});
}
}
37 changes: 37 additions & 0 deletions src/Component/DocumentTree.component.js
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 });
});
}
}
}
18 changes: 18 additions & 0 deletions src/Component/DocumentTreeBranch.component.js
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);
}
}
24 changes: 24 additions & 0 deletions src/Component/Editor.component.js
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>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

div 태그에는 name이 없답니다! 정식 스펙이 아니에요 ㅎㅎ

image

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

찾아보니 정말 그렇군요... IE에서도 호환이 되지 않으니!

id나 class값으로 충분한데 굳이 name을 쓸필요는 없겠네요ㅎㅎ;

</div>
`;
}
}
3 changes: 3 additions & 0 deletions src/Component/index.js
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";
13 changes: 13 additions & 0 deletions src/api.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",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x-username을 여기서 정의하기보단, 계층을 하나 더 만들어서 관리해주면 어떨까 싶어요! 지금은 이 유저에 종속적인 느낌이라서요 ㅎㅎ

로그인 기능이 들어온다고 생각해보세요!

"Content-Type": "application/json",
},
Comment on lines +5 to +9

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options에 headers가 있을 경우에, 여기서 정의한 headers가 덮어씌워지지 않을까요?

});

return await res.json();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 await을 해주고 안 해주고가 어떤 차이가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await을 하지 않으면 promise객체가 반환되고 , await을 한다면 content-type의 header를 json으로 바꾼뒤 요청을 다시 보내
promise가 처리되면 JSON객체를 반환할것 같습니다!

};
8 changes: 8 additions & 0 deletions src/constants.js/constants.js
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",
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

폴더 이름이 constants.js 이 되어있네요 ㅋㅋ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오잉 수정했습니다...ㅋㅋㅠㅠ

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음... 이것도 domain에서 들고있어야 하는 값이 아닐까 싶어요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변하지 않는 상수값이라고 생각해서 constant에 넣었는데 생각해보니 document에 종속적인 데이터네요!

77 changes: 77 additions & 0 deletions src/core/Component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export default class Component {
#$target;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#target 으로 써도 되지 않을까 싶어요 ㅋㅋ

#state;
#events;
#allowedProperties = ["$target", "initialState", "events"];

Choose a reason for hiding this comment

The 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();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

props 같은건 필요 없을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 properties로 받고 있는데 이것 자체가 일종의 props가 아닐까? 해서 따로 선언하지 않았습니다...!
target,initialState,events 이것들이 전부 일종의 props니까 따로 props를 선언하지 않고 props에 해당하는 것들을 전부 propperties에 담아서 보내면 되지 않을까 해서요. 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

Choose a reason for hiding this comment

The 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}`)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 동일합니다~!

callback({ event, target: event.target.closest(`${target}`) });

Choose a reason for hiding this comment

The 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;
}
}
55 changes: 55 additions & 0 deletions src/core/DomainModel.js
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} 프로퍼티는 필수로 포함되어야 합니다.`
);
}
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

property 라고 표현해야 맞지 않을까요!?

Copy link
Author

Choose a reason for hiding this comment

The 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]}`
);
}
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구조해제할당을 통해서 this의 사용을 줄여주면 좋지 않을까 싶네요 ㅋㅋ

}
}
Loading