Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a3791c7
Add: "API end point 숨김"
mxx-kor Nov 16, 2022
dfcb1a5
Add: "추가 CSS, HTML, main.js"
mxx-kor Nov 16, 2022
b729d29
Feat: "추가 validation 검사 함수, router"
mxx-kor Nov 16, 2022
cc8da73
Feat: "최상위 컴포넌트 App"
mxx-kor Nov 16, 2022
58880ea
Feat: "추가 LandingPage"
mxx-kor Nov 16, 2022
e701bc6
Feat: "Sidebar 컴포넌트 추가"
mxx-kor Nov 16, 2022
ab4ada1
Feat: "EditPage 컴포넌트"
mxx-kor Nov 16, 2022
21e6fb6
Fix: "에러 수정"
mxx-kor Nov 16, 2022
5c66953
Fix: "state 변경 후 렌더하여 오류 수정"
mxx-kor Nov 16, 2022
9b976db
Refactor: "누락된 변수 사용"
mxx-kor Nov 25, 2022
d5ef51f
Fix: "새문서 작성시 breadcrumb 오류"
mxx-kor Nov 25, 2022
99ba91b
Refactor: "중복 바인딩 한줄로 처리"
mxx-kor Nov 25, 2022
44b6780
Refactor: "HTML inline style 속성을 CSS로 수정"
mxx-kor Nov 25, 2022
95f7373
Refactor: "append를 마지막에"
mxx-kor Nov 25, 2022
7b9451e
Feat: "선택된 사이드바 내용 변경 메서드 추가"
mxx-kor Nov 25, 2022
e686a19
Feat: "하위 페이지 생성과 삭제 시 호버 스타일 정리"
mxx-kor Nov 26, 2022
df7f49e
Refactor: "breadcrumb 방어코드, 메서드 분리, validation 추가"
mxx-kor Nov 26, 2022
eeb2c72
Refactor: "조건문 괄호 처리, validation 추가"
mxx-kor Nov 26, 2022
dca573d
Refactor: "사용하지 않는 파일 정리"
mxx-kor Nov 26, 2022
2949465
Refactor: "Editor 이벤트 디스트럭팅 변수 개선"
mxx-kor Nov 26, 2022
c86403e
Refactor: "SubLink 삼항연산자로 render 수정, 방어코드 추가"
mxx-kor Nov 26, 2022
e79b992
Refactor: "class로 display 속성 변경, 불필요한 async 삭제"
mxx-kor Nov 26, 2022
d38ffa4
Refactor: "btn 클래스 추가, css 변수 추가"
mxx-kor Nov 26, 2022
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
constants.js

14 changes: 14 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title>Minjae의 Notion</title>
</head>
<body>
<main id="app"></main>
<script src="/src/main.js" type="module"></script>
</body>
</html>
82 changes: 82 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import EditorContainer from "./components/editpage/EditorContainer.js";
import Sidebar from "./components/sidebar/Sidebar.js";
import { request } from "./api.js";
import { initRouter, push } from "./router.js";
import { validation } from "./validation.js";
import LandingPage from "./components/editpage/LandingPage.js";

export default function App({ $target }) {
validation(new.target, "App");

this.state = {
documentsList: [],
editorDocument: {
docId: null,
doc: {
title: "",
content: "",
documents: [],
},
},
};

const sidebar = new Sidebar({
$target,
initialState: this.state.documentsList,
});

new LandingPage({ $target });

const editContainer = new EditorContainer({
$target,
initialState: this.state.editorDocument,
});
Comment on lines +11 to +33
Copy link
Member

Choose a reason for hiding this comment

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

initial state 관리 방식 좋네요 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

감사합니다!🙇🏻‍♂️ 필요한 state들만 사용해보려고 노력해봤습니다.


this.setState = (nextState) => {
this.state = nextState;
editContainer.setState(this.state.editorDocument);
};

this.init = async () => {
const docs = await request("/documents");
this.setState({
...this.state,
documentsList: docs,
});
await sidebar.setState(docs);
this.route();
};

this.route = () => {
const { pathname } = window.location;
const editArea = document.querySelector(".editContainer");
const landingPage = document.querySelector(".landingPage");

if (pathname === "/") {
editArea.style.display = "none";
landingPage.style.display = "block";
} else if (pathname.indexOf("/documents/") === 0) {
editArea.style.display = "block";
landingPage.style.display = "none";
const [, , docId] = pathname.split("/");
//같은 문서 두번 클릭시 내용 삭제 방지
if (this.state.editorDocument.docId === docId) {
return;
}
//뒤로가기 대비 방어코드
if (docId === null) {
push("/");
}
this.setState({
...this.state,
editorDocument: { docId },
});
}
};

this.init();

window.addEventListener("popstate", () => this.route());

initRouter(() => this.route());
}
23 changes: 23 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { API_END_POINT, X_USERNAME } from "./constants.js";

export const request = async (url, options = {}) => {
try {
const res = await fetch(`${API_END_POINT}${url}`, {
...options,
headers: {
"x-username": X_USERNAME,
"Content-Type": "application/json",
},
});

if (res.ok) {
return await res.json();
}

throw new Error("API 처리중 뭔가 이상합니다!");
} catch (e) {
alert(e.message);
history.replaceState(null, null, "/");
location.reload();
}
};
63 changes: 63 additions & 0 deletions src/components/editpage/BreadCrumb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { validation, checkDifference } from "../../validation.js";

export default function BreadCrumb({ $target, initialState, clickPath }) {
validation(new.target, "BreadCrumb");

const $breadCrumb = document.createElement("nav");
$breadCrumb.className = "linkContainer";
this.state = initialState;

this.setState = (nextState) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

state도 validation을 거치면 좋을 것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

전체적으로 validation 적용해보도록 하겠습니다. 감사합니다!

if (typeof nextState !== "object") throw new Error("변경될 상태가 객체가 아닙니다.");
if (checkDifference(this.state, nextState)) return;
this.state = nextState;
this.render();
};

const recursiveBC = (id, path) => {
const $curLi = document.getElementById(id);
if ($curLi) {
const $ul = $curLi.closest("ul");

if ($ul.className === "child" || $ul.className === "child--show") {
path.push([$curLi.querySelector("span").innerHTML, id]);

recursiveBC($ul.closest("li").id, path);
} else {
path.push([$curLi.querySelector("span").innerHTML, id]);
}
}
};

const renderBreadCrumb = (id) => {
const path = [];

recursiveBC(id, path);

return path
.reverse()
.map((el) => `<span class="link" data-id=${el[1]} >${el[0]}</span>`)
.join(" / ");
Comment on lines +37 to +40
Copy link
Member

Choose a reason for hiding this comment

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

👍

};

$breadCrumb.addEventListener("click", (e) => {
const $span = e.target.closest("span");

if ($span) {
const { id } = $span.dataset;
clickPath(id);
}
});

this.render = () => {
const { docId } = this.state;

$breadCrumb.innerHTML = `
<div>
${docId ? renderBreadCrumb(docId) : ""}
</div>
`;
};

$target.prepend($breadCrumb);
}
45 changes: 45 additions & 0 deletions src/components/editpage/Editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { validation } from "../../validation.js";

export default function Editor({ $target, initialState, onEdit }) {
validation(new.target, "Editor");
const $editor = document.createElement("div");
$editor.className = "editor";
$target.appendChild($editor);

$editor.innerHTML = `
<div style="display: flex; flex-direction: column">
<input type="text" placeholder="제목 없음" name="title" style="height:100px" />
<textarea name="content" placeholder="내용을 입력하세요" style="height:75vh;"></textarea>
</div>
`;

this.state = initialState;

this.setState = (nextState) => {
this.state = nextState;
this.render();
};

this.render = () => {
$editor.querySelector("[name=title]").value = this.state.title;
$editor.querySelector("[name=content]").value = this.state.content;
};

this.render();

$editor.addEventListener("keyup", (e) => {
const { tagName: targetTag, value } = e.target;
let nextState = {};
if (targetTag === "INPUT") {
nextState = { ...this.state, title: value };
} else {
nextState = {
...this.state,
content: value,
};
}

this.setState(nextState);
onEdit(this.state);
});
}
92 changes: 92 additions & 0 deletions src/components/editpage/EditorContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { request } from "../../api.js";
import BreadCrumb from "./BreadCrumb.js";
import Editor from "./Editor.js";
import { push } from "../../router.js";
import SubLink from "./SubLink.js";
import { validation } from "../../validation.js";

//여기에선 docId만 핸들, Editor에는 Doc만
export default function EditorContainer({ $target, initialState }) {
validation(new.target, "EditorContainer");

const $editorContainer = document.createElement("section");
$editorContainer.className = "editContainer";

this.state = initialState;

let timer = null;

const breadCrumb = new BreadCrumb({
$target: $editorContainer,
initialState: this.state,
clickPath: (id) => {
changeHoverEffect(this.state, id);
push(`/documents/${id}`);
},
});

const editor = new Editor({
$target: $editorContainer,
initialState: this.state.doc,
onEdit: (post) => {
document.getElementById(post.id).getElementsByTagName("span")[0].innerText = post.title;

if (timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(async () => {
await request(`/documents/${post.id}`, {
method: "PUT",
body: JSON.stringify(post),
});
}, 500);
},
});

const subLink = new SubLink({
$target: $editorContainer,
initialState: this.state,
clickLink: (id) => {
changeHoverEffect(this.state, id);
push(`/documents/${id}`);
},
});

this.setState = async (nextState) => {
if (this.state.docId !== nextState.docId) {
this.state = nextState;
await fetchDoc();
return;
}
this.state = nextState;

breadCrumb.setState(this.state);
subLink.setState(this.state);
editor.setState(this.state.doc || { title: "", content: "" });
this.render();
};

this.render = () => {
$target.appendChild($editorContainer);
};

const fetchDoc = async () => {
const { docId } = this.state;
const doc = await request(`/documents/${docId}`);

this.setState({
...this.state,
doc,
});
};

const changeHoverEffect = (prev, curr) => {
const { docId } = prev;
if (docId) {
const $prevLi = document.getElementById(docId);
const $currLi = document.getElementById(curr);
$prevLi.querySelector("p").style = "";
$currLi.querySelector("p").style.backgroundColor = "rgba(0, 0, 0, 0.1)";
}
};
}
20 changes: 20 additions & 0 deletions src/components/editpage/LandingPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { validation } from "../../validation.js";

export default function LandingPage({ $target }) {
validation(new.target, "LandingPage");

const $landingPage = document.createElement("section");
$landingPage.className = "landingPage";
$target.appendChild($landingPage);

this.render = () => {
$landingPage.innerHTML = `
<h1 class="landingTitle">
Notion에 오신 것을 환영합니다.
</h1>
<p class="landingP">페이지를 추가하여 새 문서를 작성해보세요.</p>
`;
};

this.render();
}
36 changes: 36 additions & 0 deletions src/components/editpage/SubLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { validation } from "../../validation.js";
export default function SubLink({ $target, initialState, clickLink }) {
validation(new.target, "SubLink");

const $subLink = document.createElement("footer");
$subLink.className = "linkContainer";
$target.appendChild($subLink);

this.state = initialState;

this.setState = (nextState) => {
this.state = nextState;
this.render();
};

this.render = () => {
const { doc } = this.state;

$subLink.innerHTML =
doc.documents.length > 0
? doc.documents.map((el) => `<div class="link" data-id=${el.id}>${el.title}</div>`).join("")
: ``;
};

$subLink.addEventListener("click", (e) => {
const { id } = e.target.closest("div").dataset;
if (id) {
const $curLi = document.getElementById(id);
const $parentUl = $curLi.closest("ul");

$parentUl.closest("li").querySelector(".toggleFold").innerText = "▼";
$parentUl.className = "child--show";
clickLink(id);
}
});
}
Loading