diff --git a/index.html b/index.html
new file mode 100644
index 00000000..a6194b9f
--- /dev/null
+++ b/index.html
@@ -0,0 +1,10 @@
+
+
+ Notion Project
+
+
+
+
+
+
+
diff --git a/src/components/App.js b/src/components/App.js
new file mode 100644
index 00000000..280329ee
--- /dev/null
+++ b/src/components/App.js
@@ -0,0 +1,42 @@
+import PostEditPage from "./PostEditPage.js";
+import PostSidebar from "./PostSidebar.js";
+import { initRouter } from "../utils/router.js";
+
+export default function App({ $target }) {
+ const $postSideBarContainer = document.createElement("div");
+ const $postEditContainer = document.createElement("div");
+ $target.appendChild($postSideBarContainer);
+ $target.appendChild($postEditContainer);
+ $postSideBarContainer.className = "post-side-bar-container";
+ $postEditContainer.className = "post-edit-container";
+
+ const postSideBar = new PostSidebar({
+ $target: $postSideBarContainer,
+ });
+
+ const postEditPage = new PostEditPage({
+ $target: $postEditContainer,
+ initialState: {
+ postId: "new",
+ post: {
+ title: "",
+ content: "",
+ },
+ },
+ });
+
+ this.route = () => {
+ const { pathname } = window.location;
+
+ postSideBar.setState();
+
+ if (pathname !== "/" && pathname.indexOf("/") === 0) {
+ const [, , postId] = pathname.split("/");
+ postEditPage.setState({ postId });
+ }
+ };
+
+ this.route();
+
+ initRouter(() => this.route());
+}
diff --git a/src/components/Editor.js b/src/components/Editor.js
new file mode 100644
index 00000000..c812978c
--- /dev/null
+++ b/src/components/Editor.js
@@ -0,0 +1,51 @@
+export default function Editor({
+ $target,
+ initialState = {
+ title: "",
+ content: "",
+ },
+ onEditing,
+}) {
+ const $editor = document.createElement("div");
+ $editor.className = "editor";
+
+ this.state = initialState;
+
+ let isInitialize = false;
+
+ $target.appendChild($editor);
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ const { title, content } = this.state;
+ $editor.querySelector("[name=title]").value = title;
+ $editor.querySelector("[name=content]").value = content;
+ this.render();
+ };
+
+ this.render = () => {
+ if (!isInitialize) {
+ $editor.innerHTML = `
+
+
+ `;
+ isInitialize = true;
+ }
+ };
+ this.render();
+
+ $editor.addEventListener("keyup", (e) => {
+ const { target } = e;
+
+ const name = target.getAttribute("name");
+
+ if (this.state[name] !== undefined) {
+ const nextState = {
+ ...this.state,
+ [name]: target.value,
+ };
+ this.setState(nextState);
+ onEditing(this.state);
+ }
+ });
+}
diff --git a/src/components/PostEditPage.js b/src/components/PostEditPage.js
new file mode 100644
index 00000000..60277afa
--- /dev/null
+++ b/src/components/PostEditPage.js
@@ -0,0 +1,62 @@
+import { getData, putData } from "../utils/api.js";
+import Editor from "./Editor.js";
+import { pushRouter } from "../utils/router.js";
+
+export default function PostEditPage({ $target, initialState }) {
+ const INTERVAL_SAVE_TIME = 2000;
+
+ this.state = initialState;
+
+ let timer = null;
+
+ const editor = new Editor({
+ $target,
+ initialState: {
+ title: "",
+ content: "",
+ },
+ onEditing: (post) => {
+ if (timer !== null) {
+ clearTimeout(timer);
+ }
+
+ timer = setTimeout(async () => {
+ const { postId } = this.state;
+ if (postId && postId !== "new") {
+ await putData(postId, post);
+ pushRouter(`/documents/${postId}`);
+ await getDocument();
+ }
+ }, INTERVAL_SAVE_TIME);
+ },
+ });
+
+ this.setState = async (nextState) => {
+ if (this.state.postId !== nextState.postId) {
+ this.state = nextState;
+ await getDocument();
+ return;
+ } else {
+ this.state = nextState;
+ }
+
+ editor.setState(
+ this.state.post || {
+ title: "",
+ content: "",
+ }
+ );
+ };
+
+ const getDocument = async () => {
+ const { postId } = this.state;
+ if (postId) {
+ const post = await getData(`/documents/${postId}`);
+
+ this.setState({
+ ...this.state,
+ post,
+ });
+ }
+ };
+}
diff --git a/src/components/PostItem.js b/src/components/PostItem.js
new file mode 100644
index 00000000..5f9160c1
--- /dev/null
+++ b/src/components/PostItem.js
@@ -0,0 +1,54 @@
+import { deleteData, getData, postData } from "../utils/api.js";
+import { pushRouter } from "../utils/router.js";
+
+export default function PostItem(title, id) {
+ const $postItemBox = document.createElement("div");
+ $postItemBox.className = id;
+
+ const $li = document.createElement("li");
+ $li.className = id;
+
+ const $title = document.createElement("span");
+ $title.className = id;
+ $title.textContent = title;
+ $li.appendChild($title);
+
+ const $addButton = makeButton("+", id);
+ $li.appendChild($addButton);
+
+ const $removeButton = makeButton("-", id);
+ $li.appendChild($removeButton);
+
+ const $postSubItemBox = document.createElement("ul");
+
+ $postItemBox.appendChild($li);
+ $postItemBox.append($postSubItemBox);
+
+ $li.addEventListener("click", async (e) => {
+ const target = e.target;
+ if (target.closest("span") === $title) {
+ pushRouter(`/documents/${$title.className}`);
+ } else if (target.closest("button") === $addButton) {
+ const createdPost = await postData($addButton.className);
+ pushRouter(`/documents/${createdPost.id}`);
+ } else if (target.closest("button") === $removeButton) {
+ alert("문서가 정상적으로 삭제되었습니다.");
+ await deleteData($removeButton.className).then((res) => {
+ if (res.parent) pushRouter(`/documents/${res.parent.id}`);
+ else {
+ pushRouter(`/`);
+ location.reload();
+ }
+ });
+ }
+ });
+
+ return { $postItemBox, $postSubItemBox };
+}
+
+export const makeButton = (text, className) => {
+ const $button = document.createElement("button");
+ $button.textContent = text;
+ $button.className = className;
+ return $button;
+};
diff --git a/src/components/PostList.js b/src/components/PostList.js
new file mode 100644
index 00000000..78343256
--- /dev/null
+++ b/src/components/PostList.js
@@ -0,0 +1,32 @@
+import PostItem from "./PostItem.js";
+
+export default function PostList({ $target, initialState }) {
+ const $postList = document.createElement("ul");
+ $target.appendChild($postList);
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ this.render();
+ };
+
+ this.makeList = ($itemContainer, data) => {
+ data.forEach(({ title, documents, id }) => {
+ const { $postItemBox, $postSubItemBox } = PostItem(title, id);
+
+ $itemContainer.appendChild($postItemBox);
+
+ if (documents.length > 0) {
+ this.makeList($postSubItemBox, documents);
+ }
+ });
+ };
+
+ this.render = () => {
+ $postList.innerHTML = "";
+ this.makeList($postList, this.state);
+ };
+
+ this.render();
+}
diff --git a/src/components/PostSidebar.js b/src/components/PostSidebar.js
new file mode 100644
index 00000000..15cd2d04
--- /dev/null
+++ b/src/components/PostSidebar.js
@@ -0,0 +1,36 @@
+import { getData, postData } from "../utils/api.js";
+import PostList from "./PostList.js";
+import { pushRouter } from "../utils/router.js";
+
+export default function PostSidebar({ $target }) {
+ const $listContainer = document.createElement("div");
+ const $createButton = document.createElement("button");
+ const $title = document.createElement("h1");
+ $listContainer.className = "post-list-container";
+ $createButton.className = "create-button";
+ $title.className = "title";
+
+ const postList = new PostList({
+ $target: $listContainer,
+ initialState: [],
+ });
+
+ this.setState = async () => {
+ const documents = await getData("/documents");
+ postList.setState(documents);
+ this.render();
+ };
+
+ this.render = async () => {
+ $createButton.textContent = "문서 생성하기";
+ $title.textContent = "Notion Project";
+ $target.appendChild($title);
+ $target.appendChild($createButton);
+ $target.appendChild($listContainer);
+ };
+
+ $createButton.addEventListener("click", async () => {
+ const createdPost = await postData();
+ pushRouter(`/documents/${createdPost.id}`);
+ });
+}
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 00000000..9db98264
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,6 @@
+import App from "./components/App.js";
+
+const $target = document.querySelector("#app");
+$target.className = "contentWrap";
+
+new App({ $target });
diff --git a/src/utils/api.js b/src/utils/api.js
new file mode 100644
index 00000000..aa20fd3f
--- /dev/null
+++ b/src/utils/api.js
@@ -0,0 +1,49 @@
+export const API_END_POINT = "https://kdt-frontend.programmers.co.kr";
+export const API_X_USERNAME = "API_X_USERNAME_LIMJISEON";
+
+export const request = async (url, options = {}) => {
+ try {
+ const response = await fetch(`${API_END_POINT}${url}`, {
+ ...options,
+ headers: {
+ "Content-Type": "application/json",
+ "x-username": API_X_USERNAME,
+ },
+ });
+
+ if (response.ok) {
+ return await response.json();
+ }
+
+ throw new Error("API 처리중 에러가 발생했습니다.");
+ } catch (e) {
+ console.error(e);
+ }
+};
+
+export const getData = async (url) => {
+ return request(url);
+};
+
+export const postData = async (id = null) => {
+ return request("/documents", {
+ method: "POST",
+ body: JSON.stringify({
+ title: "제목 없음",
+ parent: id,
+ }),
+ });
+};
+
+export const deleteData = async (id) => {
+ return request(`/documents/${id}`, {
+ method: "DELETE",
+ });
+};
+
+export const putData = async (id, post) => {
+ return request(`/documents/${id}`, {
+ method: "PUT",
+ body: JSON.stringify(post),
+ });
+};
diff --git a/src/utils/router.js b/src/utils/router.js
new file mode 100644
index 00000000..06650aee
--- /dev/null
+++ b/src/utils/router.js
@@ -0,0 +1,23 @@
+const ROUTE_CHANGE_EVENT_NAME = "route-change";
+
+export const initRouter = (onRoute) => {
+ window.addEventListener(ROUTE_CHANGE_EVENT_NAME, (e) => {
+ const { nextUrl } = e.detail;
+
+ if (nextUrl) {
+ window.addEventListener("popstate", () => onRoute());
+ history.pushState(null, null, nextUrl);
+ onRoute();
+ }
+ });
+};
+
+export const pushRouter = (nextUrl) => {
+ window.dispatchEvent(
+ new CustomEvent(ROUTE_CHANGE_EVENT_NAME, {
+ detail: {
+ nextUrl,
+ },
+ })
+ );
+};
diff --git a/style.css b/style.css
new file mode 100644
index 00000000..f062f0b0
--- /dev/null
+++ b/style.css
@@ -0,0 +1,89 @@
+body {
+ margin: 0;
+ padding: 0;
+ background-color: rgb(229, 229, 229);
+}
+
+button {
+ border: none;
+ background-color: transparent;
+ font-size: 16px;
+ margin: 0px 5px 0px 5px;
+ cursor: pointer;
+}
+
+span {
+ cursor: pointer;
+}
+
+button:hover {
+ background-color: white;
+ border-radius: 5px;
+ font-weight: bolder;
+}
+
+li {
+ padding: 10px 5px 10px 5px;
+}
+
+li:hover {
+ background-color: white;
+ border-radius: 5px;
+ font-weight: bold;
+}
+
+input {
+ width: 50vw;
+ height: 5vh;
+ border-radius: 5px;
+ font-size: 16px;
+ border: none;
+ outline: none;
+}
+
+textarea {
+ width: 50vw;
+ height: 50vh;
+ border-radius: 5px;
+ font-size: 16px;
+ border: none;
+ outline: none;
+ margin-top: 10px;
+}
+
+.contentWrap {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: space-between;
+}
+
+.post-edit-container {
+ width: 70vw;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.post-side-bar-container {
+ width: 70vw;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.create-button {
+ border-radius: 5px;
+ padding: 10px;
+ background-color: white;
+}
+
+.editor {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ padding: 0px 50px 0px 50px;
+}