diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..08e7854c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+API_END_POINT.js
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..7ce48107
--- /dev/null
+++ b/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ KDT
+
+
+
+
+
+
+
diff --git a/index.js b/index.js
new file mode 100644
index 00000000..e31ef4fd
--- /dev/null
+++ b/index.js
@@ -0,0 +1,7 @@
+import App from "./src/App.js";
+
+const $app = document.querySelector("#app");
+
+new App({
+ $target: $app,
+});
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 00000000..95b1051c
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,32 @@
+import DocsContainer from "./components/Documents/DocsContainer.js";
+import EditorContainer from "./components/Editor/EditorContainer.js";
+import { init } from "./components/utils/router/router.js";
+
+export default function App({ $target }) {
+ const docsContainer = new DocsContainer({
+ $target,
+ });
+
+ const editorContainer = new EditorContainer({
+ $target,
+ onChange: () => {
+ docsContainer.setState();
+ },
+ });
+
+ this.route = async () => {
+ const { pathname } = window.location;
+
+ docsContainer.setState();
+ if (pathname === "/") {
+ editorContainer.setState();
+ } else if (pathname.indexOf("/documents/") === 0) {
+ const [, , docId] = pathname.split("/");
+ editorContainer.setState({ id: docId });
+ }
+ };
+
+ this.route();
+
+ init(this.route);
+}
diff --git a/src/api/request.js b/src/api/request.js
new file mode 100644
index 00000000..817f38eb
--- /dev/null
+++ b/src/api/request.js
@@ -0,0 +1,17 @@
+import { API_END_POINT } from "./API_END_POINT.js";
+
+export const request = async (url, options = {}) => {
+ try {
+ const res = await fetch(`${API_END_POINT}${url}`, {
+ ...options,
+ headers: {
+ "x-username": "DevSoonyo",
+ "Content-type": "application/json",
+ },
+ });
+ if (res.ok) {
+ return await res.json();
+ }
+ throw new Error("API query error");
+ } catch (e) {}
+};
diff --git a/src/components/Documents/DocsContainer.js b/src/components/Documents/DocsContainer.js
new file mode 100644
index 00000000..c15488e9
--- /dev/null
+++ b/src/components/Documents/DocsContainer.js
@@ -0,0 +1,32 @@
+import { request } from "../../api/request.js";
+import { push } from "../utils/router/router.js";
+import DocsList from "./DocsList.js";
+
+export default function DocsContainer({ $target }) {
+ const $docsContainer = document.createElement("aside");
+ $docsContainer.className = "docs-container";
+ $target.appendChild($docsContainer);
+
+ const docsList = new DocsList({
+ $target: $docsContainer,
+ initialState: { docs: [], selectedDocs: new Set() },
+ onAdd: async ({ parent, title }) => {
+ await request("/documents", {
+ method: "POST",
+ body: JSON.stringify({ parent, title }),
+ });
+ this.setState();
+ },
+ onDelete: async ({ id }) => {
+ await request(`/documents/${id}`, {
+ method: "DELETE",
+ });
+ push("/");
+ },
+ });
+
+ this.setState = async () => {
+ const documents = await request("/documents");
+ docsList.setState({ docs: documents });
+ };
+}
diff --git a/src/components/Documents/DocsList.js b/src/components/Documents/DocsList.js
new file mode 100644
index 00000000..b1a841a8
--- /dev/null
+++ b/src/components/Documents/DocsList.js
@@ -0,0 +1,90 @@
+import { push } from "../utils/router/router.js";
+
+export default function DocsList({ $target, initialState, onAdd, onDelete }) {
+ const $docsList = document.createElement("div");
+
+ $docsList.className = "docs-list";
+
+ $target.appendChild($docsList);
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = {
+ ...this.state,
+ ...nextState,
+ };
+ this.render();
+ };
+
+ this.setMarkup = (docsList, hide = false) => {
+ const markup = `
+
+ ${docsList
+ .map((doc) => {
+ return `-
+ ${
+ doc.title.trim() === "" ? "빈 제목" : doc.title
+ }
+
+
+
+
+
+ ${
+ doc.documents && doc.documents.length > 0
+ ? this.setMarkup(
+ doc.documents,
+ !this.state.selectedDocs.has(`${doc.id}`)
+ )
+ : ""
+ }
+ `;
+ })
+ .join("")}
+
+ `;
+ return markup;
+ };
+
+ this.render = () => {
+ $docsList.innerHTML = "";
+ if (this.state.docs && this.state.docs.length > 0) {
+ $docsList.innerHTML =
+ this.setMarkup(this.state.docs) +
+ ``;
+ } else {
+ $docsList.innerHTML = `
+ No documents
+
+ `;
+ }
+ };
+
+ $docsList.addEventListener("click", (e) => {
+ const { target } = e;
+ const tempTitle = "빈 제목";
+ const $li = target.closest("li");
+ if (target.tagName === "BUTTON") {
+ const id = $li?.dataset.id;
+ if (target.name === "add") {
+ this.setState({ selectedDocs: this.state.selectedDocs.add(id) });
+ onAdd({ parent: id || null, title: tempTitle });
+ } else if (target.name === "delete") {
+ onDelete({ id });
+ }
+ } else if (target.getAttribute("name") === "item-content") {
+ const id = $li?.dataset.id;
+ const selectedDocs = this.state.selectedDocs;
+
+ // subList toggle
+ if (selectedDocs.has(id)) {
+ selectedDocs.delete(id);
+ } else {
+ selectedDocs.add(id);
+ }
+ this.setState({ selectedDocs });
+ push(`/documents/${id}`);
+ }
+ });
+}
diff --git a/src/components/Editor/EditorContainer.js b/src/components/Editor/EditorContainer.js
new file mode 100644
index 00000000..0a6f4d7e
--- /dev/null
+++ b/src/components/Editor/EditorContainer.js
@@ -0,0 +1,61 @@
+import EditorPage from "./EditorPage.js";
+import { request } from "../../api/request.js";
+import { push } from "../utils/router/router.js";
+
+export default function EditorContainer({ $target, onChange }) {
+ const $editorContainer = document.createElement("main");
+ $editorContainer.className = "editor-container";
+ $target.appendChild($editorContainer);
+
+ let timer = null;
+
+ this.state = {
+ id: null,
+ };
+
+ const $ediotr = new EditorPage({
+ $target: $editorContainer,
+ initialState: { title: "", content: "" },
+ // debounce
+ onEditing: ({ title, content }) => {
+ if (timer) {
+ clearTimeout(timer);
+ }
+
+ timer = setTimeout(async () => {
+ if (this.state.id === "new") {
+ // 새로운 문서 작성
+ const res = await request("/documents", {
+ method: "POST",
+ body: JSON.stringify({ title, parent: null }),
+ });
+ if (res) {
+ this.state = res;
+ push(`/documents/${this.state.id}`);
+ } else {
+ throw new Error("Post method failed");
+ }
+ } else {
+ // 기존 문서 수정
+ await request(`/documents/${this.state.id}`, {
+ method: "PUT",
+ body: JSON.stringify({ title, content }),
+ });
+ onChange();
+ }
+ }, 1500);
+ },
+ });
+
+ this.setState = async (selectedDoc) => {
+ const id = selectedDoc?.id;
+ if (!id) {
+ this.state = { id: "new" };
+ $ediotr.setState({ title: "", content: "" });
+ } else {
+ this.state = selectedDoc;
+ const doc = await request(`/documents/${id}`);
+ $ediotr.setState(doc);
+ }
+ };
+}
diff --git a/src/components/Editor/EditorPage.js b/src/components/Editor/EditorPage.js
new file mode 100644
index 00000000..de38872b
--- /dev/null
+++ b/src/components/Editor/EditorPage.js
@@ -0,0 +1,31 @@
+export default function EditorPage({ $target, initialState, onEditing }) {
+ const $editorPage = document.createElement("div");
+ $target.appendChild($editorPage);
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ $editorPage.querySelector("[name=title]").value = this.state.title;
+ $editorPage.querySelector("[name=content]").value = this.state.content;
+ };
+
+ this.render = () => {
+ $editorPage.innerHTML = `
+
+
+ `;
+ };
+ this.render();
+ $editorPage.addEventListener("keyup", (e) => {
+ const { target } = e;
+ const { name } = target;
+
+ const editedDoc = {
+ ...this.state,
+ [name]: target.value,
+ };
+ this.setState(editedDoc);
+ onEditing(editedDoc);
+ });
+}
diff --git a/src/components/utils/router/router.js b/src/components/utils/router/router.js
new file mode 100644
index 00000000..8ad7eecf
--- /dev/null
+++ b/src/components/utils/router/router.js
@@ -0,0 +1,22 @@
+const ROUTE_CHANGE_EVENT = "route-change";
+
+export const init = (onRoute) => {
+ window.addEventListener(ROUTE_CHANGE_EVENT, (e) => {
+ const { nextUrl } = e.detail;
+
+ if (nextUrl) {
+ history.pushState(null, null, nextUrl);
+ onRoute();
+ }
+ });
+};
+
+export const push = (nextUrl) => {
+ window.dispatchEvent(
+ new CustomEvent("route-change", {
+ detail: {
+ nextUrl,
+ },
+ })
+ );
+};
diff --git a/style.css b/style.css
new file mode 100644
index 00000000..af91524b
--- /dev/null
+++ b/style.css
@@ -0,0 +1,67 @@
+body {
+ padding: 0 0;
+ margin: 0 0;
+}
+
+.hidden {
+ display: none;
+}
+
+li {
+ margin-top: 30px;
+ font-weight: bold;
+}
+
+.item {
+ list-style: none;
+ cursor: pointer;
+}
+
+.item span:hover {
+ opacity: 50%;
+}
+
+#app {
+ display: flex;
+}
+
+.docs-container {
+ min-width: 15%;
+ height: 100vh;
+ background-color: lightgray;
+}
+
+.editor-container div {
+ height: 100vh;
+ margin-left: 100px;
+}
+.button-group {
+ display: inline-block;
+}
+
+[name="title"],
+[name="content"] {
+ display: block;
+ width: 800px;
+ border-width: 0px;
+}
+
+[name="title"] {
+ height: 5%;
+ font-size: 1.5rem;
+}
+
+[name="content"] {
+ height: 90%;
+ font-size: 1.3rem;
+}
+
+[name="title"]:focus,
+[name="content"]:focus {
+ outline: none;
+}
+
+[name="title"]:hover,
+[name="content"]:hover {
+ border: 0.5px gray dotted;
+}