diff --git a/api/api.js b/api/api.js
new file mode 100644
index 00000000..f56fd2d7
--- /dev/null
+++ b/api/api.js
@@ -0,0 +1,21 @@
+import { API_END_POINT, HEADER } from "./apiConstans.js";
+
+export const request = async (url, options = {}) => {
+ try {
+ const res = await fetch(`${API_END_POINT}${url}`, {
+ ...options,
+ headers: {
+ "Content-Type": "application/json",
+ "x-username": HEADER,
+ },
+ });
+
+ if (res.ok) {
+ return await res.json();
+ }
+
+ throw new Error("API 처리중 뭔가 이상합니다!");
+ } catch (error) {
+ alert(error.message);
+ }
+};
diff --git a/components/Content/Content.js b/components/Content/Content.js
new file mode 100644
index 00000000..30a4c947
--- /dev/null
+++ b/components/Content/Content.js
@@ -0,0 +1,99 @@
+import ContentEditor from "./ContentEditor.js";
+import { request } from "../../api/api.js";
+import { push } from "../../utils/router.js";
+
+export default function Content({ $target, initialState }) {
+ const $content = document.createElement("div");
+ $content.classList.add("layout-content");
+
+ this.state = initialState;
+
+ const post = {
+ title: "",
+ content: "",
+ };
+
+ let timer = null;
+
+ const contentEditor = new ContentEditor({
+ $target: $content,
+ initialState: post,
+ onEditing: (post) => {
+ if (timer !== null) {
+ clearTimeout(timer);
+ }
+
+ timer = setTimeout(async () => {
+ const isNew = this.state.documentId === "new";
+
+ if (isNew) {
+ const createdPost = await request("/documents", {
+ method: "post",
+ body: JSON.stringify(post),
+ });
+
+ this.setState({
+ documentId: createdPost.id,
+ });
+
+ push(`/documents/${createdPost.id}`);
+ } else {
+ await request(`/documents/${post.id}`, {
+ method: "put",
+ body: JSON.stringify(post),
+ });
+ }
+ }, 500);
+ },
+ });
+
+ this.setState = async (nextState) => {
+ if (this.state.documentId !== nextState.documentId) {
+ this.state = nextState;
+
+ if (this.state.documentId === "new") {
+ const document = {
+ title: "",
+ content: "",
+ };
+
+ contentEditor.setState(document);
+ contentEditor.render();
+ this.render();
+ } else {
+ await fetchPost();
+ }
+
+ return;
+ }
+
+ this.state = nextState;
+ this.render();
+ contentEditor.setState(
+ this.state.document || {
+ title: "",
+ content: "",
+ }
+ );
+ contentEditor.render();
+ };
+
+ this.render = () => {
+ $target.appendChild($content);
+ };
+
+ const fetchPost = async () => {
+ const { documentId } = this.state;
+
+ if (documentId !== "new") {
+ const document = await request(`/documents/${documentId}`, {
+ method: "get",
+ });
+
+ this.setState({
+ ...this.state,
+ document,
+ });
+ }
+ };
+}
diff --git a/components/Content/ContentEditor.js b/components/Content/ContentEditor.js
new file mode 100644
index 00000000..de3d925b
--- /dev/null
+++ b/components/Content/ContentEditor.js
@@ -0,0 +1,51 @@
+export default function ContentEditor({
+ $target,
+ initialState = {
+ title: "",
+ content: "",
+ },
+ onEditing,
+}) {
+ const $contentEditor = document.createElement("div");
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ };
+
+ $contentEditor.innerHTML = `
+
+
+ `;
+
+ $target.appendChild($contentEditor);
+
+ this.render = () => {
+ $contentEditor.querySelector("[name=title]").value = this.state.title;
+ $contentEditor.querySelector("[name=content]").value = this.state.content;
+ };
+
+ this.render();
+
+ $contentEditor.addEventListener("keyup", (e) => {
+ const { target } = e;
+ const name = target.getAttribute("name");
+ const nextState = {
+ ...this.state,
+ [name]: target.value,
+ };
+
+ this.setState(nextState);
+ onEditing(this.state);
+ });
+}
diff --git a/components/SideBar/SideBar.js b/components/SideBar/SideBar.js
new file mode 100644
index 00000000..c7b08ddb
--- /dev/null
+++ b/components/SideBar/SideBar.js
@@ -0,0 +1,25 @@
+import SideBarList from "./SideBarList.js";
+import { request } from "../../api/api.js";
+
+export default function SideBar({ $target }) {
+ const $sideBar = document.createElement("div");
+ $sideBar.classList.add("layout-sidebar");
+
+ const sideBarList = new SideBarList({
+ $target: $sideBar,
+ initialState: [],
+ });
+
+ this.setState = async () => {
+ const documents = await request("/documents", {
+ method: "get",
+ });
+
+ sideBarList.setState(documents);
+ this.render();
+ };
+
+ this.render = async () => {
+ $target.appendChild($sideBar);
+ };
+}
diff --git a/components/SideBar/SideBarItem.js b/components/SideBar/SideBarItem.js
new file mode 100644
index 00000000..31274c6e
--- /dev/null
+++ b/components/SideBar/SideBarItem.js
@@ -0,0 +1,14 @@
+export default function SideBarItem(item) {
+ return `
+
+
+ ${item.title}
+
+
+
+
+
+ `;
+}
diff --git a/components/SideBar/SideBarList.js b/components/SideBar/SideBarList.js
new file mode 100644
index 00000000..290e403f
--- /dev/null
+++ b/components/SideBar/SideBarList.js
@@ -0,0 +1,63 @@
+import SideBarItem from "./SideBarItem.js";
+import { request } from "../../api/api.js";
+import { push } from "../../utils/router.js";
+
+export default function SideBarList({ $target, initialState }) {
+ const $sideBarList = document.createElement("div");
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ this.render();
+ };
+
+ this.render = () => {
+ if (!this.state) return;
+
+ $sideBarList.innerHTML = `
+
+ `;
+
+ $target.appendChild($sideBarList);
+ };
+
+ this.render();
+
+ $sideBarList.addEventListener("click", async (e) => {
+ const $item = e.target;
+ const { id } = $item.parentElement.dataset;
+
+ if ($item.className === "item-load") {
+ push(`/documents/${id}`);
+
+ return;
+ }
+
+ if ($item.className === "item-remove") {
+ await request(`/documents/${id}`, {
+ method: "delete",
+ });
+
+ push(`/`);
+
+ return;
+ }
+
+ if ($item.className === "item-add") {
+ const tempPost = {
+ title: "",
+ parent: id,
+ };
+ const createdPost = await request("/documents", {
+ method: "post",
+ body: JSON.stringify(tempPost),
+ });
+
+ // this.setState()
+ push(`/documents/${createdPost.id}`);
+ }
+ });
+}
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..4d714f06
--- /dev/null
+++ b/index.html
@@ -0,0 +1,10 @@
+
+
+ Notion
+
+
+
+
+
+
+
diff --git a/index.js b/index.js
new file mode 100644
index 00000000..acf90b87
--- /dev/null
+++ b/index.js
@@ -0,0 +1,5 @@
+import App from "./pages/App.js";
+
+const $target = document.querySelector("#app");
+
+new App({ $target });
diff --git a/pages/App.js b/pages/App.js
new file mode 100644
index 00000000..d4cd5eef
--- /dev/null
+++ b/pages/App.js
@@ -0,0 +1,40 @@
+import SideBar from "../components/SideBar/SideBar.js";
+import Content from "../components/Content/Content.js";
+import { initRouter } from "../utils/router.js";
+
+export default function App({ $target }) {
+ const $layout = document.createElement("div");
+ $layout.classList.add("layout-main");
+
+ const sideBar = new SideBar({
+ $target: $layout,
+ });
+ const content = new Content({
+ $target: $layout,
+ initialState: {
+ documentId: "new",
+ },
+ });
+
+ this.route = async () => {
+ $target.innerHTML = ``;
+
+ const { pathname } = window.location;
+
+ if (pathname === "/") {
+ await sideBar.setState();
+ await content.setState({ documentId: "new" });
+ } else if (pathname.indexOf("/documents/" === 0)) {
+ const [, , documentId] = pathname.split("/");
+
+ await sideBar.setState();
+ await content.setState({ documentId });
+ }
+
+ $target.appendChild($layout);
+ };
+
+ this.route();
+
+ initRouter(() => this.route());
+}
diff --git a/style/style.css b/style/style.css
new file mode 100644
index 00000000..0bc79744
--- /dev/null
+++ b/style/style.css
@@ -0,0 +1,41 @@
+.layout-main {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ color: #37352f;
+ background: #ffffff;
+}
+
+.layout-sidebar {
+ width: 20%;
+ max-width: 280px;
+ min-width: 200px;
+ height: 100%;
+ font-size: 14px;
+ font-weight: bold;
+ color: #37352f;
+ background: #fbfbfa;
+ overflow: auto;
+ float: left;
+}
+
+.layout-content {
+ width: 80%;
+ height: 100%;
+ float: left;
+}
+
+.sidebar-list-ul {
+ list-style: none;
+}
+
+.editor-title {
+ width: 100%;
+ height: 5%;
+ max-height: 60px;
+}
+
+.editor-content {
+ width: 100%;
+ height: 95%;
+}
diff --git a/utils/localStorage.js b/utils/localStorage.js
new file mode 100644
index 00000000..ace8af8b
--- /dev/null
+++ b/utils/localStorage.js
@@ -0,0 +1,19 @@
+const localStorage = window.localStorage;
+
+export const getItem = (key, defaultValue) => {
+ try {
+ const storedValue = localStorage.getItem(key);
+
+ return storedValue ? JSON.parse(storedValue) : defaultValue;
+ } catch (error) {
+ return defaultValue;
+ }
+};
+
+export const setItem = (key, value) => {
+ localStorage.setItem(key, JSON.stringify(value));
+};
+
+export const removeItem = (key) => {
+ localStorage.removeItem(key);
+};
diff --git a/utils/router.js b/utils/router.js
new file mode 100644
index 00000000..67482796
--- /dev/null
+++ b/utils/router.js
@@ -0,0 +1,22 @@
+const ROUTE_CHANGE_EVENT_NAME = "route-change";
+
+export const initRouter = (onRoute) => {
+ window.addEventListener(ROUTE_CHANGE_EVENT_NAME, (e) => {
+ const { nextUrl } = e.detail;
+
+ if (nextUrl) {
+ history.pushState(null, null, nextUrl);
+ onRoute();
+ }
+ });
+};
+
+export const push = (nextUrl) => {
+ window.dispatchEvent(
+ new CustomEvent(ROUTE_CHANGE_EVENT_NAME, {
+ detail: {
+ nextUrl,
+ },
+ })
+ );
+};