diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..b7d71608
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+ApiKey.js
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..60bc1f83
--- /dev/null
+++ b/index.html
@@ -0,0 +1,15 @@
+
+
+
+ Notion_cloning
+
+
+
+
+
+
+
+
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 00000000..7b4d2007
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,52 @@
+import DocumentPage from "../src/components/DocumentPage.js";
+import DocumentEditPage from "../src/components/DocumentEditPage.js";
+import { editorRoute } from "../src/utils/router.js";
+import MainPage from "../src/pages/MainPage.js";
+import { removeDiv } from "./utils/removeDocumentList.js";
+
+export default function App({ $target }) {
+ const documentPage = new DocumentPage({
+ $target,
+ });
+
+ const documentEditPage = new DocumentEditPage({
+ $target,
+ initialState: {
+ documentId: null,
+ document: {
+ title: "",
+ content: "",
+ },
+ },
+ });
+
+ const mainPage = new MainPage({
+ $target,
+ initialState: "주다현",
+ });
+
+ this.render = () => {
+ documentPage.render();
+ };
+
+ this.render();
+
+ this.route = (parent) => {
+ const { pathname } = window.location;
+ if (pathname === "/") {
+ removeDiv(".edit-page");
+ mainPage.render();
+ } else {
+ removeDiv(".main-page");
+ const [, id] = pathname.split("/");
+ documentEditPage.setState({
+ documentId: id,
+ parentId: parent,
+ });
+ }
+ };
+
+ this.route();
+
+ editorRoute((parent) => this.route(parent));
+}
diff --git a/src/api/Api.js b/src/api/Api.js
new file mode 100644
index 00000000..1de58666
--- /dev/null
+++ b/src/api/Api.js
@@ -0,0 +1,19 @@
+import API_BASE_URL from "../utils/ApiKey.js";
+
+export const request = async (url, options = {}) => {
+ try {
+ const res = await fetch(`${API_BASE_URL}${url}`, {
+ ...options,
+ headers: {
+ "Content-Type": "application/json",
+ "x-username": "dahyeon-Ju",
+ },
+ });
+
+ if (res.ok) return await res.json();
+
+ throw new Error("API 처리 실패");
+ } catch (error) {
+ alert(error.message);
+ }
+};
diff --git a/src/components/DocumentEditPage.js b/src/components/DocumentEditPage.js
new file mode 100644
index 00000000..34ddc4ab
--- /dev/null
+++ b/src/components/DocumentEditPage.js
@@ -0,0 +1,116 @@
+import Editor from "./Editor.js";
+import { setItem, getItem, removeItem } from "../utils/storage.js";
+import { request } from "../api/api.js";
+import { push } from "../utils/router.js";
+
+export default function DocumentEditPage({ $target, initialState }) {
+ const $editPage = document.createElement("div");
+ $editPage.className = "edit-page";
+ this.state = initialState;
+
+ let documentLocalSaveKey = `temp-document-${this.state.documentId}`;
+
+ const tempDocument = getItem(documentLocalSaveKey, {
+ title: "",
+ content: "",
+ });
+
+ let timer = null;
+
+ const editor = new Editor({
+ $target: $editPage,
+ initialState: tempDocument || {
+ title: "",
+ content: "",
+ },
+ onEditing: (document) => {
+ if (timer !== null) {
+ clearTimeout(timer);
+ }
+
+ timer = setTimeout(async () => {
+ setItem(documentLocalSaveKey, {
+ ...document,
+ tempSaveData: new Date(),
+ });
+
+ await request(`/${this.state.documentId}`, {
+ method: "PUT",
+ body: JSON.stringify(document),
+ });
+
+ if (this.state.document.title) {
+ if (this.state.document.title !== document.title) {
+ push({
+ type: null,
+ id: null,
+ });
+ }
+ }
+ removeItem(documentLocalSaveKey);
+ }, 500);
+ },
+ });
+
+ this.setState = async (nextState) => {
+ if (this.state.documentId !== nextState.documentId) {
+ documentLocalSaveKey = `temp-document-${this.state.documentId}`;
+ this.state = nextState;
+
+ if (this.state.documentId === "new") {
+ editor.setState(
+ this.state.document || {
+ title: "",
+ content: "",
+ }
+ );
+ } else {
+ await fetchDocument();
+ }
+
+ return;
+ }
+
+ this.state = nextState;
+ const document = this.state.document;
+ if (document) {
+ if (document.title === "제목없음") {
+ editor.setState({
+ title: "",
+ content: this.state.document.content,
+ });
+ } else {
+ editor.setState(
+ this.state.document || {
+ title: "",
+ content: "",
+ }
+ );
+ }
+ }
+
+ this.render();
+ };
+
+ this.render = () => {
+ $target.appendChild($editPage);
+ };
+
+ const fetchDocument = async () => {
+ const { documentId } = this.state;
+
+ if (this.state.documentId !== null) {
+ const document = await request(`/${documentId}`);
+
+ const getTempDocument = getItem(documentLocalSaveKey, {
+ title: "",
+ content: "",
+ });
+
+ this.setState({
+ ...this.state,
+ document,
+ });
+ }
+ };
+}
diff --git a/src/components/DocumentList.js b/src/components/DocumentList.js
new file mode 100644
index 00000000..50e2eaad
--- /dev/null
+++ b/src/components/DocumentList.js
@@ -0,0 +1,54 @@
+import { displayDocumentList } from "../utils/displayDocumentList.js";
+import { push } from "../utils/router.js";
+import { setItem, getItem, removeItem } from "../utils/storage.js";
+
+export default function PostList({ $target, initialState }) {
+ const $documentList = document.createElement("div");
+ $documentList.className = "document-list";
+ $target.appendChild($documentList);
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ this.render();
+ };
+
+ this.render = () => {
+ $documentList.innerHTML = `
+
+
+ ${displayDocumentList(this.state)}
+
+ `;
+ };
+
+ this.render();
+
+ $documentList.addEventListener("click", (e) => {
+ const { target } = e;
+ const element = target.closest("[name]");
+
+ if (element) {
+ const id = element.dataset.id;
+ const listToggleState = `isOpened-${id}`;
+ const name = element.getAttribute("name");
+ if (target.className === "toggle-btn") {
+ const getToggleState = getItem(listToggleState);
+ getToggleState
+ ? removeItem(listToggleState)
+ : setItem(listToggleState, "block");
+ this.render();
+
+ return;
+ }
+
+ if (name) {
+ push({
+ type: name,
+ id,
+ });
+ }
+ }
+ });
+}
diff --git a/src/components/DocumentPage.js b/src/components/DocumentPage.js
new file mode 100644
index 00000000..8276c71b
--- /dev/null
+++ b/src/components/DocumentPage.js
@@ -0,0 +1,36 @@
+import DocumentList from "./DocumentList.js";
+import { request } from "../api/api.js";
+import Header from "./Header.js";
+import { listRoute } from "../utils/router.js";
+
+//Sidebar
+export default function documentPage({ $target }) {
+ const $documentPage = document.createElement("div");
+ $documentPage.className = "document-page";
+
+ new Header({
+ $target: $documentPage,
+ initialState: "주다현",
+ });
+
+ const documentList = new DocumentList({
+ $target: $documentPage,
+ initialState: [],
+ });
+
+ const fetchDocuments = async () => {
+ const documents = await request("/");
+ documentList.setState(documents);
+ };
+
+ this.render = async () => {
+ await fetchDocuments();
+ $target.prepend($documentPage);
+ };
+
+ this.route = () => {
+ this.setState();
+ };
+
+ listRoute(() => fetchDocuments());
+}
diff --git a/src/components/Editor.js b/src/components/Editor.js
new file mode 100644
index 00000000..1f8b51db
--- /dev/null
+++ b/src/components/Editor.js
@@ -0,0 +1,48 @@
+export default function Editor({
+ $target,
+ initialState = {
+ titile: "",
+ content: "",
+ },
+ onEditing,
+}) {
+ const $editor = document.createElement("div");
+ $editor.className = "editor";
+ $target.appendChild($editor);
+ let isinitialize = false;
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ $editor.querySelector("[name = title]").value = this.state.title;
+ $editor.querySelector("[name = content]").value = this.state.content;
+ this.render();
+ };
+
+ this.render = () => {
+ if (!isinitialize) {
+ $editor.innerHTML = `
+
+
+ `;
+ isinitialize = true;
+ }
+ };
+
+ this.render();
+
+ $editor.addEventListener("keypress", (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/Header.js b/src/components/Header.js
new file mode 100644
index 00000000..3460c308
--- /dev/null
+++ b/src/components/Header.js
@@ -0,0 +1,23 @@
+import { push } from "../utils/router.js";
+
+export default function Header({ $target, initialState }) {
+ const $header = document.createElement("h3");
+ $target.appendChild($header);
+ this.state = initialState;
+
+ this.render = () => {
+ $header.innerHTML = `🧡 ${this.state}'s Notion 🧡`;
+ };
+
+ this.render();
+
+ $header.addEventListener("click", (e) => {
+ const { target } = e;
+ if (target) {
+ push({
+ type: "header",
+ id: null,
+ });
+ }
+ });
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 00000000..3876e42a
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,5 @@
+import App from "./App.js";
+
+const $target = document.querySelector("#app");
+
+new App({ $target });
diff --git a/src/pages/MainPage.js b/src/pages/MainPage.js
new file mode 100644
index 00000000..bbac4c45
--- /dev/null
+++ b/src/pages/MainPage.js
@@ -0,0 +1,14 @@
+export default function MainPage({ $target, initialState }) {
+ const $mainPage = document.createElement("div");
+ $mainPage.className = "main-page";
+
+ this.state = initialState;
+
+ $mainPage.innerHTML = `${this.state}님, Notion에 오신 것을 환영합니다.
+
+ 문서를 추가하여 작업을 시작해보세요
`;
+
+ this.render = () => {
+ $target.appendChild($mainPage);
+ };
+}
diff --git a/src/style/style.css b/src/style/style.css
new file mode 100644
index 00000000..05f281ac
--- /dev/null
+++ b/src/style/style.css
@@ -0,0 +1,147 @@
+* {
+ font-family: "Spoqa Han Sans Neo", "sans-serif";
+ margin: 0;
+}
+
+body {
+ color: #333333;
+ overflow: hidden;
+}
+
+#app {
+ display: grid;
+ grid-template-columns: 1fr 4.5fr;
+ overflow: hidden;
+}
+h3 {
+ margin: 10px;
+ text-align: center;
+}
+
+.document-page {
+ background-color: #f7f6f3;
+ padding: 8% 0;
+ height: 100vh;
+ width: 30vh;
+}
+/* #e8e7e4 */
+.edit-page {
+ background-color: #ffffff;
+ text-align: center;
+}
+.root-page-add {
+ width: 90%;
+ text-align: center;
+}
+
+.editor {
+ display: grid;
+ width: 55%;
+ padding: 8% 20%;
+}
+.editor-title {
+ width: 100%;
+ font-size: 2em;
+ font-style: bold;
+ font-weight: 600;
+}
+.editor-content {
+ width: 100%;
+ height: 470px;
+ font-size: 1.1em;
+ overflow: scroll;
+ /* scroll-bar :none; */
+}
+
+.editor-content::-webkit-scrollbar {
+ display: none;
+}
+button {
+ background-color: transparent;
+ border: none;
+ color: gray;
+}
+
+ul {
+ /* background-color:skyblue; */
+ width: 100%;
+ list-style: none;
+ padding: 0 3%;
+}
+
+.document-list {
+ font-size: 0.98em;
+ margin-top: 2%;
+
+ /* scroll-bar :none; */
+}
+.list-box {
+ /* width: 100%; */
+ overflow: scroll;
+ margin: 5px;
+}
+
+.list-box::-webkit-scrollbar {
+ display: none;
+}
+.root-page-add {
+ font-size: 1.1em;
+ margin: 10px;
+}
+
+input:focus {
+ outline: none;
+}
+
+textarea:focus {
+ outline: none;
+}
+
+textarea {
+ border: none;
+ text-align: justify;
+ line-height: 1.8;
+}
+
+input {
+ border: none;
+ margin-bottom: 1%;
+}
+
+.main-page {
+ text-align: center;
+ padding: 20%;
+}
+.main-title {
+ font-size: 1.5em;
+ font-weight: 900;
+}
+
+.main-content {
+ font-size: 1.2em;
+ padding-top: 1.2%;
+}
+li {
+ /* background-color:#e8e7e4; */
+ cursor: pointer;
+ margin: 10px;
+}
+/* li :hover{
+} */
+
+button {
+ cursor: pointer;
+ text-align: center;
+}
+.list-btn-box {
+ float: right;
+ margin-right: 5px;
+}
+
+.no-page {
+ color: #c6c6c6;
+}
+
+.toggle-btn:visited {
+ transform: rotate(45deg);
+}
diff --git a/src/utils/displayDocumentList.js b/src/utils/displayDocumentList.js
new file mode 100644
index 00000000..d3d6c8a3
--- /dev/null
+++ b/src/utils/displayDocumentList.js
@@ -0,0 +1,45 @@
+import { getItem } from "./storage.js";
+
+export const displayDocumentList = (documents) => {
+ return `
+ ${documents
+ .map((document) => {
+ const listToggleState = `isOpened-${document.id}`;
+ const display = getItem(listToggleState) || "none";
+ return `
+ -
+ ${
+ !listToggleState
+ ? `
+
+
+ ${document.title}
+ `
+ : `
+
+
+ ${document.title}
+ `
+ }
+
+
+ ${
+ document.documents.length
+ ? `
${displayDocumentList(
+ document.documents
+ )}
`
+ : ``
+ }
+
+ `;
+ })
+ .join("")}
`;
+};
diff --git a/src/utils/removeDocumentList.js b/src/utils/removeDocumentList.js
new file mode 100644
index 00000000..e0b9622f
--- /dev/null
+++ b/src/utils/removeDocumentList.js
@@ -0,0 +1,6 @@
+export const removeDiv = (className) => {
+ const $pageDiv = document.querySelector(className);
+ if ($pageDiv) {
+ $pageDiv.remove();
+ }
+};
diff --git a/src/utils/router.js b/src/utils/router.js
new file mode 100644
index 00000000..36d02a1c
--- /dev/null
+++ b/src/utils/router.js
@@ -0,0 +1,64 @@
+import { request } from "../api/api.js";
+
+const ROUTE_EVENT_NAME = "click-event-route-change";
+const ROUTE_EDIT_EVENT = "edit-event-route-change";
+
+export const editorRoute = (onRoute) => {
+ window.addEventListener(ROUTE_EVENT_NAME, async (e) => {
+ const { type } = e.detail;
+ const { id } = e.detail;
+ const { pathname } = window.location;
+ const [, documentId] = pathname.split("/");
+
+ if (type === "list" && documentId !== id) {
+ if (id == null) {
+ id = documentId;
+ }
+ history.pushState(null, null, `/${id}`);
+ onRoute(null);
+ } else if (type === "remove-btn") {
+ if (documentId === id) {
+ history.pushState(null, null, "/");
+ }
+ onRoute(null);
+ } else if (type === "add-btn") {
+ const createdDocument = await request("/", {
+ method: "POST",
+ body: JSON.stringify({
+ title: "제목없음",
+ parent: id,
+ }),
+ });
+ history.pushState(null, null, `/${createdDocument.id}`);
+ onRoute(id);
+ } else if (type === "header") {
+ history.pushState(null, null, "/");
+ onRoute(null);
+ }
+ });
+};
+
+export const listRoute = (onRoute) => {
+ window.addEventListener(ROUTE_EVENT_NAME, async (e) => {
+ const { type } = e.detail;
+ const { id } = e.detail;
+
+ if (type === "remove-btn") {
+ await request(`/${id}`, {
+ method: "DELETE",
+ });
+ }
+ onRoute();
+ });
+};
+
+export const push = (element) => {
+ window.dispatchEvent(
+ new CustomEvent(ROUTE_EVENT_NAME, {
+ detail: {
+ type: element.type || null,
+ id: element.id || null,
+ },
+ })
+ );
+};
diff --git a/src/utils/storage.js b/src/utils/storage.js
new file mode 100644
index 00000000..9716dac8
--- /dev/null
+++ b/src/utils/storage.js
@@ -0,0 +1,22 @@
+const storage = window.localStorage;
+
+export const setItem = (key, value) => {
+ try {
+ storage.setItem(key, JSON.stringify(value));
+ } catch (e) {
+ console.log(e);
+ }
+};
+
+export const getItem = (key, defaultValue) => {
+ const storedValue = JSON.parse(storage.getItem(key)) || defaultValue;
+ return storedValue;
+};
+
+export const removeItem = (key) => {
+ try {
+ storage.removeItem(key);
+ } catch (e) {
+ console.log(e.message);
+ }
+};