diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..a11c21ab
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/etc.txt
\ No newline at end of file
diff --git a/README.md b/README.md
index 82b85e81..d19818cc 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,17 @@
-# ๐ 4์ฃผ์ฐจ ํ๋ก์ ํธ[Project1]
+# VanillaJS๋ก ๋ง๋ ๋
ธ์
ํด๋ก๋ ํ๋ก์ ํธ
-## ํ์ ํ๋ก์ ํธ
+## ๋ฐฐํฌ ์ฃผ์
+[๐ณ JooNotion](https://joo-notion.vercel.app/)
-- ํ๋ก์ ํธ ๊ธฐํ
- - ํ๋ก์ ํธ ์ํ ๊ธฐ๊ฐ : 2022๋
11์ 8์ผ(ํ) ~ 2022๋
11์ 16์ผ(์)
- - ๋ฉํฐ ์ฝ๋ ๋ฆฌ๋ทฐ ๊ธฐ๊ฐ : 2022๋
11์ 17์ผ(๋ชฉ) ~ 2022๋
11์ 20์ผ(์ผ)
- - ๋ฉํ ์ฝ๋ ๋ฆฌ๋ทฐ ๊ธฐ๊ฐ : 2022๋
11์ 17์ผ(๋ชฉ) ~ 2022๋
11์ 22์ผ(ํ)
- - ์ฝ๋ ๋ฆฌ๋ทฐ ๋ฐ์ ๊ธฐ๊ฐ : 2022๋
11์ 23์ผ(์) ~ 2022๋
11์ 25์ผ(๊ธ)
-- ๋ด์ฉ
- - **Day 17 [ํ๋ก์ ํธ] ๋
ธ์
ํด๋ก๋ ์๊ตฌ์ฌํญ** ํ์ธ ๋ถํ๋๋ฆฝ๋๋ค.
+## ๊ธฐ๋ฅ ๊ตฌํ
+- History API๋ฅผ ์ด์ฉํด SPA ํํ๋ก ๊ฐ๋ฐ
+- ๋ฌธ์ ์์ฑ, ์์ , ์ญ์
+- ํ์๋ฌธ์ ์์ฑ, ์์ , ์ญ์
+ - ํ์๋ฌธ์์ ํ ๊ธ ์ํ๋ฅผ LocalStorage์ ์ ์ฅํ์ฌ ์๋ก๊ณ ์นจํด๋ ์ ์ง๋๋๋ก ๊ตฌํ
+- ๋ฌธ์ ํธ์ง ์ API ํธ์ถ์ ์ค์ด๊ธฐ ์ํด ๋๋ฐ์ด์ฑ ๊ตฌํ
+ - ๋ง์ง๋ง ์
๋ ฅ ํ 1์ด๊ฐ ์
๋ ฅ์ด ์์ผ๋ฉด ์๋ ์ ์ฅ
+
+## ํ๋ก์ ํธ ํ๊ณ
+ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉฐ ์๊ธด ๋ฌธ์ ์ ๋ํด ๊ณ ๋ฏผํ๊ณ ํด๊ฒฐ๊ณผ์ ์ ๊ธฐ๋กํ์ต๋๋ค.
+
+[๋ธ๋ก๊ทธ ์ฃผ์](https://woogie-97.tistory.com/10)
diff --git a/config.js b/config.js
new file mode 100644
index 00000000..d3ab90d7
--- /dev/null
+++ b/config.js
@@ -0,0 +1 @@
+export const OPENED_LIST_KEY = "local-save-open-list";
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..03c31891
--- /dev/null
+++ b/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+ ๐ณ JooNotion
+
+
+
+
+
+
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 00000000..cf56108f
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,89 @@
+import PostEdit from "./components/PostEdit.js";
+import PostsPage from "./components/PostsPage.js";
+import HomePage from "./pages/HomePage.js";
+import { initRouter } from "./routes/router.js";
+import NotFound from "./pages/NotFound.js";
+import { request } from "./api/index.js";
+
+export default function App({ $target }) {
+ const $postListContainer = document.createElement("div");
+ const $homeContainer = document.createElement("div");
+ const $postEditContainer = document.createElement("div");
+
+ $homeContainer.className = "post-edit-container";
+ $postListContainer.className = "post-list-container";
+ $postEditContainer.className = "post-edit-container";
+
+ $target.appendChild($postListContainer);
+ $target.appendChild($homeContainer);
+ $target.appendChild($postEditContainer);
+
+ this.state = {
+ selectedPost: {},
+ };
+
+ const homePage = new HomePage({
+ $target: $homeContainer,
+ });
+
+ const notFound = new NotFound({
+ $target,
+ });
+
+ /**
+ * PostList ๊ฐ์ธ๋ ์ปดํฌ๋ํธ
+ */
+ const postsPage = new PostsPage({
+ $target: $postListContainer,
+ });
+
+ /**
+ * Editor ๊ฐ์ธ๋ ์ปดํฌ๋ํธ
+ */
+ const postEdit = new PostEdit({
+ $target: $postEditContainer,
+ initialState: this.state,
+ updatePost: (updatePost) => {
+ postsPage.setState(updatePost);
+ },
+ });
+
+ postsPage.setState();
+
+ this.init = async () => {
+ const { pathname } = window.location;
+ if (pathname === "/") {
+ $postEditContainer.style.display = "none";
+ $homeContainer.style.display = "flex";
+ } else if (pathname.indexOf("/documents/") === 0) {
+ $postEditContainer.style.display = "flex";
+ $homeContainer.style.display = "none";
+
+ const [, , id] = pathname.split("/");
+ if (id === "new") {
+ postEdit.setState({
+ ...this.state,
+ id,
+ });
+ } else {
+ const post = await request(`/documents/${id}`, {
+ method: "GET",
+ });
+ postsPage.setState(post);
+ postEdit.setState(post);
+ }
+ } else {
+ notFound.render();
+ $postListContainer.style.display = "none";
+ $postEditContainer.style.display = "none";
+ $homeContainer.style.display = "none";
+ }
+ };
+
+ this.init();
+ initRouter(() => this.init());
+
+ window.addEventListener("popstate", () => {
+ this.init();
+ });
+}
diff --git a/src/api/index.js b/src/api/index.js
new file mode 100644
index 00000000..8b9cfa97
--- /dev/null
+++ b/src/api/index.js
@@ -0,0 +1,19 @@
+const API_END_POINT = "https://kdt-frontend.programmers.co.kr";
+
+export const request = async (url, options = {}) => {
+ try {
+ const res = await fetch(`${API_END_POINT}${url}`, {
+ ...options,
+ headers: {
+ "Content-Type": "application/json",
+ "x-username": "woogie",
+ },
+ });
+ if (res.ok) {
+ return await res.json();
+ }
+ throw new Error("API ํธ์ถ ์คํจ!!!!!!!");
+ } catch (error) {
+ alert(error);
+ }
+};
diff --git a/src/assets/css/fontello.css b/src/assets/css/fontello.css
new file mode 100644
index 00000000..9bad1512
--- /dev/null
+++ b/src/assets/css/fontello.css
@@ -0,0 +1,61 @@
+@font-face {
+ font-family: 'fontello';
+ src: url('../font/fontello.eot?10205068');
+ src: url('../font/fontello.eot?10205068#iefix') format('embedded-opentype'),
+ url('../font/fontello.woff2?10205068') format('woff2'),
+ url('../font/fontello.woff?10205068') format('woff'),
+ url('../font/fontello.ttf?10205068') format('truetype'),
+ url('../font/fontello.svg?10205068#fontello') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
+/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
+/*
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ @font-face {
+ font-family: 'fontello';
+ src: url('../font/fontello.svg?10205068#fontello') format('svg');
+ }
+}
+*/
+[class^="icon-"]:before, [class*=" icon-"]:before {
+ font-family: "fontello";
+ font-style: normal;
+ font-weight: normal;
+ speak: never;
+
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+
+ /* Font smoothing. That was taken from TWBS */
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
+}
+
+.icon-trash:before { content: '\e803'; } /* '๎ ' */
+.icon-plus-1:before { content: '\e804'; } /* '๎ ' */
+.icon-down-open:before { content: '\f004'; } /* '๏' */
+.icon-right-open:before { content: '\f006'; } /* '๏' */
+.icon-circle-thin:before { content: '\f1db'; } /* '๏' */
diff --git a/src/assets/css/libs/_mixins.scss b/src/assets/css/libs/_mixins.scss
new file mode 100644
index 00000000..d98ffa3d
--- /dev/null
+++ b/src/assets/css/libs/_mixins.scss
@@ -0,0 +1,52 @@
+@import "./vars.scss";
+
+@mixin base-button {
+ button {
+ font-family: "Noto Sans KR", Sans-serif;
+ border: 0;
+ color: $color-base-font;
+ background-color: transparent;
+ }
+
+ button:hover {
+ cursor: pointer;
+ }
+}
+
+@mixin input-title {
+ .title {
+ cursor: text;
+ font-size: 2.5rem;
+ font-weight: 700;
+ line-height: 4rem;
+ outline: none;
+ }
+
+ .title:empty:before {
+ content: attr(placeholder);
+ font-size: 2.5rem;
+ font-weight: 700;
+ line-height: 4rem;
+ color: #ccc;
+ outline: none;
+ }
+}
+
+@mixin input-content {
+ .content {
+ height: 600px;
+ cursor: text;
+ font-size: 1rem;
+ line-height: 3rem;
+ outline: none;
+ }
+
+ .content:empty::before {
+ content: attr(placeholder);
+ top: 0;
+ font-size: 1rem;
+ line-height: 3rem;
+ color: #ccc;
+ outline: none;
+ }
+}
diff --git a/src/assets/css/libs/_vars.scss b/src/assets/css/libs/_vars.scss
new file mode 100644
index 00000000..a82526b3
--- /dev/null
+++ b/src/assets/css/libs/_vars.scss
@@ -0,0 +1,16 @@
+// font
+$color-home-font: #c7c7c7;
+$color-base-font: #5e5e5e;
+
+// background
+$color-post-edit-background: #ffffff;
+$color-post-list-background: rgb(251 251 250);
+
+// hover
+$color-post-list-hover: rgb(235, 235, 235);
+$color-post-button-hover: rgb(209, 209, 209);
+
+// button
+$color-modal-close-btn-background: #6b6b6b;
+$color-post-create-btn-background: rgb(35, 131, 226);
+$color-white: #ffffff;
diff --git a/src/assets/css/modal.scss b/src/assets/css/modal.scss
new file mode 100644
index 00000000..6e28e958
--- /dev/null
+++ b/src/assets/css/modal.scss
@@ -0,0 +1,75 @@
+@import "./libs/vars.scss";
+@import "./libs/mixins.scss";
+
+.container-modal {
+ display: none;
+
+ .modal {
+ z-index: 2;
+ position: absolute;
+ max-width: 970px;
+ margin: 0 auto;
+ width: 100%;
+ height: 400px;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: $color-post-edit-background;
+ padding: 2rem 4rem;
+ border-radius: 4px;
+ box-shadow: rgb(15 15 15 / 5%) 0px 0px 0px 1px,
+ rgb(15 15 15 / 10%) 0px 5px 10px, rgb(15 15 15 / 20%) 0px 15px 40px;
+
+ .modal-title {
+ position: relative;
+
+ h2 {
+ font-size: 1.5rem;
+ font-weight: bold;
+ }
+
+ .title-input-container {
+ position: relative;
+ margin-top: 1.5rem;
+ height: 300px;
+
+ @include input-title;
+ }
+ }
+
+ .modal-btn-container {
+ display: flex;
+ justify-content: end;
+ gap: 1rem;
+
+ button {
+ padding: 8px 1rem;
+ border-radius: 10px;
+ color: $color-white;
+ font-size: 1rem;
+ }
+
+ .close {
+ background-color: $color-modal-close-btn-background;
+ }
+
+ .create-post-btn {
+ background-color: $color-post-create-btn-background;
+ }
+ }
+ }
+
+ .overlay {
+ z-index: 1;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.4);
+ }
+}
+
+.post-modal-show {
+ display: block;
+}
diff --git a/src/assets/css/postList.scss b/src/assets/css/postList.scss
new file mode 100644
index 00000000..281aab7f
--- /dev/null
+++ b/src/assets/css/postList.scss
@@ -0,0 +1,132 @@
+@import "./libs/vars.scss";
+@import "./libs/mixins.scss";
+
+.post-list-container {
+ background-color: $color-post-list-background;
+ max-width: 300px;
+ min-width: 260px;
+ width: 100%;
+
+ .nav-header {
+ padding: 1rem 0;
+ text-align: center;
+ }
+
+ .nav-header:hover {
+ cursor: pointer;
+ }
+
+ @include base-button;
+
+ ul {
+ max-height: 870px;
+ overflow-y: scroll;
+ .post-list {
+ font-size: 14px;
+ padding-left: 4px;
+ color: $color-base-font;
+
+ .list-flex {
+ display: flex;
+ position: relative;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 4px;
+
+ .open-folder:hover {
+ background-color: $color-post-button-hover;
+ }
+
+ span {
+ padding: 2px;
+ }
+
+ .post-title {
+ display: inline-block;
+ vertical-align: bottom;
+ max-width: 150px;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .delete-create-btn-container {
+ display: none;
+ position: absolute;
+ right: 4px;
+
+ button {
+ padding: 1px;
+ }
+ }
+ }
+
+ .list-flex:hover {
+ cursor: pointer;
+ background-color: $color-post-list-hover;
+
+ .delete-create-btn-container {
+ display: block;
+ z-index: 5;
+
+ button {
+ cursor: pointer;
+ }
+
+ button:hover {
+ background-color: $color-post-button-hover;
+ }
+ }
+ }
+
+ .list-active {
+ background-color: $color-post-list-hover;
+ }
+
+ .child-ul {
+ padding-left: 24px;
+ }
+
+ .empty {
+ color: $color-home-font;
+ padding: 8px 0 8px 24px;
+ }
+
+ .hide {
+ display: none;
+ }
+ }
+ }
+
+ .empty-posts-list {
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+ gap: 10px;
+ padding-top: 3rem;
+ }
+
+ .create-post-btn-container {
+ padding: 1rem 0;
+ width: 300px;
+ text-align: center;
+ position: fixed;
+ bottom: 0;
+ background-color: $color-post-list-background;
+
+ button {
+ color: #000000;
+ font-size: 14px;
+ font-weight: bold;
+ }
+
+ button:hover {
+ cursor: pointer;
+ }
+ }
+
+ .create-post-btn-container:hover {
+ cursor: pointer;
+ background-color: $color-post-list-hover;
+ }
+}
diff --git a/src/assets/css/reset.css b/src/assets/css/reset.css
new file mode 100644
index 00000000..9e40b8d4
--- /dev/null
+++ b/src/assets/css/reset.css
@@ -0,0 +1,130 @@
+html,
+body,
+div,
+span,
+applet,
+object,
+iframe,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+blockquote,
+pre,
+a,
+abbr,
+acronym,
+address,
+big,
+cite,
+code,
+del,
+dfn,
+em,
+img,
+ins,
+kbd,
+q,
+s,
+samp,
+small,
+strike,
+strong,
+sub,
+sup,
+tt,
+var,
+b,
+u,
+i,
+center,
+dl,
+dt,
+dd,
+ol,
+ul,
+li,
+fieldset,
+form,
+label,
+legend,
+table,
+caption,
+tbody,
+tfoot,
+thead,
+tr,
+th,
+td,
+article,
+aside,
+canvas,
+details,
+embed,
+figure,
+figcaption,
+footer,
+header,
+hgroup,
+menu,
+nav,
+output,
+ruby,
+section,
+summary,
+time,
+mark,
+audio,
+video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+
+/* HTML5 display-role reset for older browsers */
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+menu,
+nav,
+section {
+ display: block;
+}
+
+body {
+ line-height: 1;
+}
+
+ol,
+ul {
+ list-style: none;
+}
+
+blockquote,
+q {
+ quotes: none;
+}
+
+blockquote:before,
+blockquote:after,
+q:before,
+q:after {
+ content: "";
+ content: none;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
diff --git a/src/assets/css/style.css b/src/assets/css/style.css
new file mode 100644
index 00000000..0e08b35c
--- /dev/null
+++ b/src/assets/css/style.css
@@ -0,0 +1,298 @@
+@import "./reset.css";
+.container-modal {
+ display: none;
+}
+.container-modal .modal {
+ z-index: 2;
+ position: absolute;
+ max-width: 970px;
+ margin: 0 auto;
+ width: 100%;
+ height: 400px;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: #ffffff;
+ padding: 2rem 4rem;
+ border-radius: 4px;
+ box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 5px 10px, rgba(15, 15, 15, 0.2) 0px 15px 40px;
+}
+.container-modal .modal .modal-title {
+ position: relative;
+}
+.container-modal .modal .modal-title h2 {
+ font-size: 1.5rem;
+ font-weight: bold;
+}
+.container-modal .modal .modal-title .title-input-container {
+ position: relative;
+ margin-top: 1.5rem;
+ height: 300px;
+}
+.container-modal .modal .modal-title .title-input-container .title {
+ cursor: text;
+ font-size: 2.5rem;
+ font-weight: 700;
+ line-height: 4rem;
+ outline: none;
+}
+.container-modal .modal .modal-title .title-input-container .title:empty:before {
+ content: attr(placeholder);
+ font-size: 2.5rem;
+ font-weight: 700;
+ line-height: 4rem;
+ color: #ccc;
+ outline: none;
+}
+.container-modal .modal .modal-btn-container {
+ display: flex;
+ justify-content: end;
+ gap: 1rem;
+}
+.container-modal .modal .modal-btn-container button {
+ padding: 8px 1rem;
+ border-radius: 10px;
+ color: #ffffff;
+ font-size: 1rem;
+}
+.container-modal .modal .modal-btn-container .close {
+ background-color: #6b6b6b;
+}
+.container-modal .modal .modal-btn-container .create-post-btn {
+ background-color: rgb(35, 131, 226);
+}
+.container-modal .overlay {
+ z-index: 1;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.4);
+}
+
+.post-modal-show {
+ display: block;
+}
+
+.post-list-container {
+ background-color: rgb(251, 251, 250);
+ max-width: 300px;
+ min-width: 260px;
+ width: 100%;
+}
+.post-list-container .nav-header {
+ padding: 1rem 0;
+ text-align: center;
+}
+.post-list-container .nav-header:hover {
+ cursor: pointer;
+}
+.post-list-container button {
+ font-family: "Noto Sans KR", Sans-serif;
+ border: 0;
+ color: #5e5e5e;
+ background-color: transparent;
+}
+.post-list-container button:hover {
+ cursor: pointer;
+}
+.post-list-container ul {
+ max-height: 870px;
+ overflow-y: scroll;
+}
+.post-list-container ul .post-list {
+ font-size: 14px;
+ padding-left: 4px;
+ color: #5e5e5e;
+}
+.post-list-container ul .post-list .list-flex {
+ display: flex;
+ position: relative;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 4px;
+}
+.post-list-container ul .post-list .list-flex .open-folder:hover {
+ background-color: rgb(209, 209, 209);
+}
+.post-list-container ul .post-list .list-flex span {
+ padding: 2px;
+}
+.post-list-container ul .post-list .list-flex .post-title {
+ display: inline-block;
+ vertical-align: bottom;
+ max-width: 150px;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.post-list-container ul .post-list .list-flex .delete-create-btn-container {
+ display: none;
+ position: absolute;
+ right: 4px;
+}
+.post-list-container ul .post-list .list-flex .delete-create-btn-container button {
+ padding: 1px;
+}
+.post-list-container ul .post-list .list-flex:hover {
+ cursor: pointer;
+ background-color: rgb(235, 235, 235);
+}
+.post-list-container ul .post-list .list-flex:hover .delete-create-btn-container {
+ display: block;
+ z-index: 5;
+}
+.post-list-container ul .post-list .list-flex:hover .delete-create-btn-container button {
+ cursor: pointer;
+}
+.post-list-container ul .post-list .list-flex:hover .delete-create-btn-container button:hover {
+ background-color: rgb(209, 209, 209);
+}
+.post-list-container ul .post-list .list-active {
+ background-color: rgb(235, 235, 235);
+}
+.post-list-container ul .post-list .child-ul {
+ padding-left: 24px;
+}
+.post-list-container ul .post-list .empty {
+ color: #c7c7c7;
+ padding: 8px 0 8px 24px;
+}
+.post-list-container ul .post-list .hide {
+ display: none;
+}
+.post-list-container .empty-posts-list {
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+ gap: 10px;
+ padding-top: 3rem;
+}
+.post-list-container .create-post-btn-container {
+ padding: 1rem 0;
+ width: 300px;
+ text-align: center;
+ position: fixed;
+ bottom: 0;
+ background-color: rgb(251, 251, 250);
+}
+.post-list-container .create-post-btn-container button {
+ color: #000000;
+ font-size: 14px;
+ font-weight: bold;
+}
+.post-list-container .create-post-btn-container button:hover {
+ cursor: pointer;
+}
+.post-list-container .create-post-btn-container:hover {
+ cursor: pointer;
+ background-color: rgb(235, 235, 235);
+}
+
+.global-container {
+ display: flex;
+ height: 100vh;
+ font-family: "Noto Sans KR", Sans-serif;
+}
+.global-container .post-list-container,
+.global-container .post-edit-container {
+ display: inline-block;
+}
+.global-container .post-edit-container {
+ display: flex;
+ width: 100%;
+ max-width: 900px;
+ min-width: 400px;
+ margin: 0 auto;
+ justify-content: center;
+ align-items: center;
+ padding: 0 4rem;
+ background-color: #ffffff;
+}
+.global-container .post-edit-container .home h1 {
+ font-size: 3rem;
+ font-weight: bold;
+ color: #c7c7c7;
+}
+.global-container .post-edit-container .editor {
+ width: 100%;
+ max-height: 850px;
+ overflow-y: scroll;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ padding-right: 3rem;
+}
+.global-container .post-edit-container .editor .title-input-container {
+ padding-top: 5rem;
+}
+.global-container .post-edit-container .editor .title-input-container .title {
+ cursor: text;
+ font-size: 2.5rem;
+ font-weight: 700;
+ line-height: 4rem;
+ outline: none;
+}
+.global-container .post-edit-container .editor .title-input-container .title:empty:before {
+ content: attr(placeholder);
+ font-size: 2.5rem;
+ font-weight: 700;
+ line-height: 4rem;
+ color: #ccc;
+ outline: none;
+}
+.global-container .post-edit-container .editor .post-date-container {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ text-align: end;
+}
+.global-container .post-edit-container .editor .post-date-container span {
+ color: #c7c7c7;
+}
+.global-container .post-edit-container .editor .content-input-container .content {
+ height: 600px;
+ cursor: text;
+ font-size: 1rem;
+ line-height: 3rem;
+ outline: none;
+}
+.global-container .post-edit-container .editor .content-input-container .content:empty::before {
+ content: attr(placeholder);
+ top: 0;
+ font-size: 1rem;
+ line-height: 3rem;
+ color: #ccc;
+ outline: none;
+}
+.global-container .post-edit-container .editor::-webkit-scrollbar {
+ display: none;
+}
+.global-container .not-found {
+ width: 100%;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+}
+.global-container .not-found h1 {
+ font-size: 2rem;
+ font-weight: bold;
+ margin-bottom: 1rem;
+}
+.global-container .not-found button {
+ font-family: "Noto Sans KR", Sans-serif;
+ border: 0;
+ color: #5e5e5e;
+ background-color: transparent;
+}
+.global-container .not-found button:hover {
+ cursor: pointer;
+}
+.global-container .not-found button {
+ font-size: 1rem;
+}
+
+/*# sourceMappingURL=style.css.map */
diff --git a/src/assets/css/style.css.map b/src/assets/css/style.css.map
new file mode 100644
index 00000000..ab697246
--- /dev/null
+++ b/src/assets/css/style.css.map
@@ -0,0 +1 @@
+{"version":3,"sourceRoot":"","sources":["style.scss","modal.scss","libs/_vars.scss","libs/_mixins.scss","postList.scss"],"names":[],"mappings":"AAAQ;ACGR;EACC;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kBCX2B;EDY3B;EACA;EACA;;AAGA;EACC;;AAEA;EACC;EACA;;AAGD;EACC;EACA;EACA;;AEjBH;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;AFSA;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA,OChCU;EDiCV;;AAGD;EACC,kBCvC+B;;AD0ChC;EACC,kBC1C+B;;AD+ClC;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIF;EACC;;;AGtED;EACC,kBFE4B;EED5B;EACA;EACA;;AAEA;EACC;EACA;;AAGD;EACC;;ADZD;EACC;EACA;EACA,ODJgB;ECKhB;;AAGD;EACC;;ACSD;EACC;EACA;;AACA;EACC;EACA;EACA,OFxBe;;AE0Bf;EACC;EACA;EACA;EACA;EACA;;AAEA;EACC,kBF1BqB;;AE6BtB;EACC;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;;AAEA;EACC;;AAKH;EACC;EACA,kBFxDoB;;AE0DpB;EACC;EACA;;AAEA;EACC;;AAGD;EACC,kBFlEoB;;AEuEvB;EACC,kBFzEoB;;AE4ErB;EACC;;AAGD;EACC,OFzFc;EE0Fd;;AAGD;EACC;;AAKH;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA,kBF5G2B;;AE8G3B;EACC;EACA;EACA;;AAGD;EACC;;AAIF;EACC;EACA,kBFxHsB;;;AFFxB;EACC;EACA;EACA;;AAEA;AAAA;EAEC;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kBErB2B;;AFwB1B;EACC;EACA;EACA,OE/Bc;;AFmChB;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;;AG9BH;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;AHoBC;EACC;EACA;EACA;EACA;;AAEA;EACC,OExDa;;ACkCjB;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;AHiBA;EACC;;AAIF;EACC;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;;AG/EF;EACC;EACA;EACA,ODJgB;ECKhB;;AAGD;EACC;;AH4EA;EACC","file":"style.css"}
\ No newline at end of file
diff --git a/src/assets/css/style.scss b/src/assets/css/style.scss
new file mode 100644
index 00000000..75abf789
--- /dev/null
+++ b/src/assets/css/style.scss
@@ -0,0 +1,92 @@
+@import "./reset.css";
+@import "./libs/vars.scss";
+@import "./libs/mixins.scss";
+
+@import "./modal.scss";
+@import "./postList.scss";
+
+.global-container {
+ display: flex;
+ height: 100vh;
+ font-family: "Noto Sans KR", Sans-serif;
+
+ .post-list-container,
+ .post-edit-container {
+ display: inline-block;
+ }
+
+ .post-edit-container {
+ display: flex;
+ width: 100%;
+ max-width: 900px;
+ min-width: 400px;
+ margin: 0 auto;
+ justify-content: center;
+ align-items: center;
+ padding: 0 4rem;
+ background-color: $color-post-edit-background;
+
+ .home {
+ h1 {
+ font-size: 3rem;
+ font-weight: bold;
+ color: $color-home-font;
+ }
+ }
+
+ .editor {
+ width: 100%;
+ max-height: 850px;
+ overflow-y: scroll;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ padding-right: 3rem;
+
+ .title-input-container {
+ padding-top: 5rem;
+ @include input-title;
+ }
+
+ .post-date-container {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ text-align: end;
+
+ span {
+ color: $color-home-font;
+ }
+ }
+
+ .content-input-container {
+ @include input-content;
+ }
+ }
+
+ .editor::-webkit-scrollbar {
+ display: none;
+ }
+ }
+
+ .not-found {
+ width: 100%;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+
+ h1 {
+ font-size: 2rem;
+ font-weight: bold;
+ margin-bottom: 1rem;
+ }
+
+ @include base-button;
+
+ button {
+ font-size: 1rem;
+ }
+ }
+}
diff --git a/src/assets/font/fontello.eot b/src/assets/font/fontello.eot
new file mode 100644
index 00000000..ac02012c
Binary files /dev/null and b/src/assets/font/fontello.eot differ
diff --git a/src/assets/font/fontello.svg b/src/assets/font/fontello.svg
new file mode 100644
index 00000000..e67ea068
--- /dev/null
+++ b/src/assets/font/fontello.svg
@@ -0,0 +1,20 @@
+
+
+
diff --git a/src/assets/font/fontello.ttf b/src/assets/font/fontello.ttf
new file mode 100644
index 00000000..6adc48fa
Binary files /dev/null and b/src/assets/font/fontello.ttf differ
diff --git a/src/assets/font/fontello.woff b/src/assets/font/fontello.woff
new file mode 100644
index 00000000..b15dbc46
Binary files /dev/null and b/src/assets/font/fontello.woff differ
diff --git a/src/assets/font/fontello.woff2 b/src/assets/font/fontello.woff2
new file mode 100644
index 00000000..0735f65e
Binary files /dev/null and b/src/assets/font/fontello.woff2 differ
diff --git a/src/components/CreatePostButton.js b/src/components/CreatePostButton.js
new file mode 100644
index 00000000..4738ff5c
--- /dev/null
+++ b/src/components/CreatePostButton.js
@@ -0,0 +1,37 @@
+import { $createPostBtn } from "../utils/templates.js";
+import CreatePostModal from "./modal/CreatePostModal.js";
+
+export default function CreatePostButton({
+ $target,
+ initialState,
+ createPost,
+}) {
+ const $createPostBtnContainer = document.createElement("div");
+ $createPostBtnContainer.className = "create-post-btn-container";
+ $target.appendChild($createPostBtnContainer);
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ };
+
+ this.render = () => {
+ $createPostBtnContainer.innerHTML = $createPostBtn();
+ };
+
+ this.render();
+
+ $createPostBtnContainer.addEventListener("click", () => {
+ $createPostModal.setState({
+ link: "/documents/new",
+ modalOpen: true,
+ });
+ });
+
+ const $createPostModal = new CreatePostModal({
+ $target,
+ initialState: { link: "/documents/new", modalOpen: false },
+ createPost,
+ });
+}
diff --git a/src/components/Editor.js b/src/components/Editor.js
new file mode 100644
index 00000000..53bc3cdc
--- /dev/null
+++ b/src/components/Editor.js
@@ -0,0 +1,42 @@
+import { $editPost } from "../utils/templates.js";
+
+export default function Editor({ $target, initialState, onEditing }) {
+ const $editor = document.createElement("div");
+ $editor.className = "editor";
+ this.state = initialState;
+ this.body = initialState;
+
+ $target.appendChild($editor);
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ this.body = nextState;
+ this.render();
+ $editor.querySelector('div[name="title"]').innerText = this.state.title;
+ $editor.querySelector('div[name="content"]').innerHTML =
+ this.state.content === null
+ ? ""
+ : this.state.content.replace(/\n/g, "
");
+ };
+
+ this.render = () => {
+ $editor.innerHTML = $editPost(this.state);
+ };
+
+ $editor.addEventListener("keyup", (e) => {
+ const { target } = e;
+
+ const post = {
+ id: this.body.id,
+ title: this.body.title,
+ content: this.body.content,
+ };
+ if (target.className.includes("title")) {
+ this.body.title = target.innerText;
+ } else {
+ this.body.content = target.innerText;
+ }
+
+ onEditing(post);
+ });
+}
diff --git a/src/components/PostEdit.js b/src/components/PostEdit.js
new file mode 100644
index 00000000..45cbb3c9
--- /dev/null
+++ b/src/components/PostEdit.js
@@ -0,0 +1,62 @@
+import { request } from "../api/index.js";
+import Editor from "./Editor.js";
+
+export default function PostEdit({ $target, initialState, updatePost }) {
+ this.state = initialState;
+
+ let timer = null;
+ const editor = new Editor({
+ $target,
+ initialState: this.state,
+ onEditing: (post) => {
+ if (timer !== null) {
+ clearTimeout(timer);
+ }
+ timer = setTimeout(async () => {
+ const updatedPost = await request(`/documents/${post.id}`, {
+ method: "PUT",
+ body: JSON.stringify(post),
+ });
+ updatePost(updatedPost);
+ }, 1000);
+ },
+ });
+
+ this.setState = async (nextState) => {
+ if (this.state.id !== nextState.id) {
+ this.state = nextState;
+ if (this.state.id === "new") {
+ this.render();
+ editor.setState(this.state);
+ } else {
+ await fetchPost();
+ }
+ return;
+ }
+
+ this.state = nextState;
+ this.render();
+
+ editor.setState(
+ this.state || {
+ title: "",
+ content: "",
+ }
+ );
+ };
+
+ this.render = () => {
+ editor.setState(this.state);
+ };
+
+ const fetchPost = async () => {
+ const { id } = this.state;
+ if (id !== "new") {
+ const post = await request(`/documents/${id}`);
+ this.setState({
+ ...this.state,
+ post,
+ });
+ }
+ };
+}
diff --git a/src/components/PostList.js b/src/components/PostList.js
new file mode 100644
index 00000000..7a8913e8
--- /dev/null
+++ b/src/components/PostList.js
@@ -0,0 +1,123 @@
+import { OPENED_LIST_KEY } from "../../config.js";
+import { getItem, removeItem, setItem } from "../utils/storage.js";
+import { $onLoadParentList } from "../utils/templates.js";
+import CreatePostModal from "./modal/CreatePostModal.js";
+
+export default function PostList({
+ $target,
+ removePost,
+ clickPost,
+ createPost,
+}) {
+ const $postList = document.createElement("div");
+ const $emptyPostDiv = document.createElement("div");
+ $emptyPostDiv.className = "empty-posts-list";
+
+ $target.appendChild($postList);
+
+ this.state = {
+ posts: [],
+ selectedPostId: null,
+ openedId: [],
+ };
+ this.setState = ({ postsList, selectedPost }) => {
+ this.state.posts = postsList;
+ this.state.selectedPostId =
+ selectedPost === undefined ? null : selectedPost.id;
+ this.state.openedId = getItem(OPENED_LIST_KEY, []);
+ this.render();
+ };
+
+ this.setOpenedLists = (nextState) => {
+ this.state.openedId = [...nextState];
+ setItem(OPENED_LIST_KEY, this.state.openedId);
+ };
+
+ this.render = () => {
+ if (this.state.posts.length > 0) {
+ $postList.innerHTML = `${$onLoadParentList(
+ this.state.posts,
+ this.state.selectedPostId,
+ this.state.openedId
+ )}`;
+ } else {
+ $postList.innerHTML = "";
+ $postList.appendChild($emptyPostDiv);
+ $emptyPostDiv.innerHTML = `
+ ์์ง ์์ฑ๋ ๊ธ์ด ์์ด์.
+ ์ค๋ ํ๋ฃจ๋ฅผ ๊ธฐ๋กํด๋ณด์ธ์! ๐
+
+ `;
+ }
+ };
+
+ $postList.addEventListener("click", (e) => {
+ const $li = e.target.closest(".list-flex");
+ const $childLi = $li.nextElementSibling;
+
+ if ($li && $li.dataset.id !== undefined) {
+ const { className } = e.target;
+ const { id } = $li.dataset;
+
+ if (className.includes("open-folder")) {
+ if (className.includes("icon-right-open")) {
+ e.target.classList.replace("icon-right-open", "icon-down-open");
+ this.state.openedId.includes(id)
+ ? null
+ : this.setOpenedLists([...this.state.openedId, id]);
+ } else {
+ e.target.classList.replace("icon-down-open", "icon-right-open");
+ this.setOpenedLists(
+ this.state.openedId.filter((item) => item !== id)
+ );
+ }
+
+ if ($childLi.className.includes("hide")) {
+ $childLi.classList.remove("hide");
+ } else {
+ $childLi.classList.add("hide");
+ }
+ } else if (className.includes("delete-page-btn")) {
+ removePost(id, this.state.openedId);
+ } else if (className.includes("create-child-page-btn")) {
+ $createPostModal.setState({
+ link: `/documents/${id}`,
+ modalOpen: true,
+ createdChildPost: (newChildId) => {
+ if (newChildId) {
+ this.setOpenedLists([...this.state.openedId, id]);
+ $childLi.classList.remove("hide");
+ }
+ },
+ });
+ } else {
+ clickPost(id);
+ }
+ }
+ });
+
+ /**
+ * ํฌ์คํธ ๋ฆฌ์คํธ ๋น์ด์์๋
+ * ๊ธ ์์ฑ ๋ฒํผ ํด๋ฆญ
+ */
+ $emptyPostDiv.addEventListener("click", (e) => {
+ const { className } = e.target;
+ if (className.includes("first-create-page-btn")) {
+ $createPostModal.setState({
+ link: "/documents/new",
+ modalOpen: true,
+ });
+ }
+ });
+
+ /**
+ * ๋ฃจํธ ํ์ด์ง ์์ฑ ๋ชจ๋ฌ
+ */
+ const $createPostModal = new CreatePostModal({
+ $target,
+ initialState: { link: `/documents/new`, modalOpen: false },
+ createPost,
+ });
+
+ this.render();
+}
diff --git a/src/components/PostsPage.js b/src/components/PostsPage.js
new file mode 100644
index 00000000..435f0cad
--- /dev/null
+++ b/src/components/PostsPage.js
@@ -0,0 +1,60 @@
+import PostsPageHeader from "./PostsPageHeader.js";
+import CreatePostButton from "./CreatePostButton.js";
+import PostList from "./PostList.js";
+import { request } from "../api/index.js";
+import { push } from "../routes/router.js";
+
+export default function PostsPage({ $target }) {
+ const $postPage = document.createElement("div");
+
+ this.setState = async (nextState) => {
+ const postsList = await request("/documents", {
+ method: "GET",
+ });
+ postList.setState({ postsList, selectedPost: nextState });
+ this.render();
+ };
+
+ new PostsPageHeader({ $target: $postPage });
+ const postList = new PostList({
+ $target: $postPage,
+
+ removePost: async (id, openedId) => {
+ const post = await request(`/documents/${id}`, {
+ method: "GET",
+ });
+ if (confirm(`์ ๋ชฉ : ${post.title}\nํด๋น ๊ธ์ ์ญ์ ํ ๊น์?`)) {
+ const res = await request(`/documents/${post.id}`, {
+ method: "DELETE",
+ });
+ if (res.id) {
+ this.setState();
+ postList.setOpenedLists(openedId.filter((item) => item !== id));
+ push("/");
+ }
+ }
+ },
+
+ clickPost: (id) => {
+ push(`/documents/${id}`);
+ },
+
+ createPost: (createdPost) => {
+ this.setState(createdPost);
+ },
+ });
+
+ new CreatePostButton({
+ $target,
+ initialState: { link: "/documents/new", modalOpen: false },
+ createPost: (createdPost) => {
+ this.setState(createdPost);
+ },
+ });
+
+ this.render = () => {
+ $target.appendChild($postPage);
+ };
+
+ this.render();
+}
diff --git a/src/components/PostsPageHeader.js b/src/components/PostsPageHeader.js
new file mode 100644
index 00000000..b37cf113
--- /dev/null
+++ b/src/components/PostsPageHeader.js
@@ -0,0 +1,17 @@
+import { $postsPageHeader } from "../utils/templates.js";
+
+export default function PostsPageHeader({ $target }) {
+ const $navHeader = document.createElement("div");
+
+ $target.appendChild($navHeader);
+
+ this.render = () => {
+ $navHeader.innerHTML = $postsPageHeader();
+ };
+
+ $navHeader.addEventListener("click", () => {
+ window.location = window.location.origin;
+ });
+
+ this.render();
+}
diff --git a/src/components/modal/CreatePostModal.js b/src/components/modal/CreatePostModal.js
new file mode 100644
index 00000000..490a915f
--- /dev/null
+++ b/src/components/modal/CreatePostModal.js
@@ -0,0 +1,76 @@
+import { $createPostModal } from "../../utils/templates.js";
+import { modalClose, modalShow } from "./handler.js";
+import { push } from "../../routes/router.js";
+import { request } from "../../api/index.js";
+
+export default function CreatePostModal({ $target, initialState, createPost }) {
+ const $modal = document.createElement("div");
+ $target.appendChild($modal);
+
+ this.state = initialState;
+ this.body = { title: "" };
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ if (this.state.modalOpen) {
+ modalShow($modal);
+ modalEventListener();
+ } else modalClose($modal);
+ };
+
+ this.setBody = (nextState) => {
+ this.body = nextState;
+ };
+
+ const modalEventListener = () => {
+ const $overlay = document.querySelectorAll(".overlay");
+ $overlay.forEach((item) => {
+ item.addEventListener("click", () => {
+ modalClose($modal);
+ });
+ });
+ };
+
+ this.render = () => {
+ $modal.innerHTML = $createPostModal();
+ };
+
+ $modal.addEventListener("keyup", (e) => {
+ const { target } = e;
+ const name = target.getAttribute("name");
+
+ let state = { ...this.state };
+ const [, , id] = state.link.split("/");
+ this.setBody({
+ parent: id === "new" ? null : id,
+ [name]: target.innerText,
+ });
+ });
+
+ $modal.addEventListener("click", async (e) => {
+ const { target } = e;
+ const $closeBtn = target.closest(".close");
+ const $createBtn = target.closest(".create-post-btn");
+
+ if ($createBtn) {
+ if (this.body.title.length === 0) {
+ alert("๊ธ ์ ๋ชฉ์ ์
๋ ฅํด์ฃผ์ธ์!");
+ return;
+ }
+ const createdPost = await request("/documents", {
+ method: "POST",
+ body: JSON.stringify(this.body),
+ });
+ if (createdPost.id) {
+ modalClose($modal);
+ push(`/documents/${createdPost.id}`);
+ this.state.createdChildPost(createdPost.id);
+ createPost(createdPost);
+ }
+ } else if ($closeBtn) {
+ modalClose($modal);
+ }
+ });
+
+ this.render();
+}
diff --git a/src/components/modal/handler.js b/src/components/modal/handler.js
new file mode 100644
index 00000000..92c5ea75
--- /dev/null
+++ b/src/components/modal/handler.js
@@ -0,0 +1,12 @@
+// ๋ชจ๋ฌ ์
๋ ฅ ์ฐฝ ๋ซ๊ธฐ
+const modalClose = ($target) => {
+ $target.querySelector(".container-modal").classList.remove("post-modal-show");
+ $target.querySelector('div[name="title"]').innerText = "";
+};
+
+// ๋ชจ๋ฌ ์
๋ ฅ ์ฐฝ ๋์ฐ๊ธฐ
+const modalShow = ($target) => {
+ $target.querySelector(".container-modal").classList.add("post-modal-show");
+};
+
+export { modalShow, modalClose };
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/HomePage.js b/src/pages/HomePage.js
new file mode 100644
index 00000000..bb1ec099
--- /dev/null
+++ b/src/pages/HomePage.js
@@ -0,0 +1,13 @@
+import { $home } from "../utils/templates.js";
+
+export default function HomePage({ $target }) {
+ const $homePage = document.createElement("div");
+
+ $target.appendChild($homePage);
+
+ this.render = () => {
+ $homePage.innerHTML = $home();
+ };
+
+ this.render();
+}
diff --git a/src/pages/NotFound.js b/src/pages/NotFound.js
new file mode 100644
index 00000000..911b0d93
--- /dev/null
+++ b/src/pages/NotFound.js
@@ -0,0 +1,24 @@
+export default function NotFound({ $target }) {
+ const $notFound = document.createElement("div");
+ $notFound.className = "not-found";
+
+ $target.appendChild($notFound);
+
+ this.render = () => {
+ $notFound.innerHTML = `
+ ํ์ด์ง๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. ๐ข
+
+ `;
+ };
+
+ this.init = () => {
+ $notFound.addEventListener("click", (e) => {
+ const $button = e.target.closest("button");
+ if ($button) {
+ window.location = window.location.origin;
+ }
+ });
+ };
+
+ this.init();
+}
diff --git a/src/routes/router.js b/src/routes/router.js
new file mode 100644
index 00000000..090b5bcc
--- /dev/null
+++ b/src/routes/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,
+ },
+ })
+ );
+};
diff --git a/src/utils/storage.js b/src/utils/storage.js
new file mode 100644
index 00000000..b95c768d
--- /dev/null
+++ b/src/utils/storage.js
@@ -0,0 +1,18 @@
+const storage = window.localStorage;
+
+export const setItem = (key, value) => {
+ storage.setItem(key, JSON.stringify(value));
+};
+
+export const getItem = (key, defaultValue) => {
+ try {
+ const storedValue = storage.getItem(key);
+ return storedValue ? JSON.parse(storedValue) : defaultValue;
+ } catch (error) {
+ return defaultValue;
+ }
+};
+
+export const removeItem = (key) => {
+ storage.removeItem(key);
+};
diff --git a/src/utils/templates.js b/src/utils/templates.js
new file mode 100644
index 00000000..10e748b7
--- /dev/null
+++ b/src/utils/templates.js
@@ -0,0 +1,209 @@
+const $home = () => {
+ return `
+
+
๐ณ JooNotion
+
+ `;
+};
+
+const formatTime = (time) => {
+ if (isNaN(time)) return;
+
+ const date = `
+ ${time.getFullYear()}-${String(time.getMonth() + 1).padStart(2, "0")}-${String(
+ time.getDate()
+ ).padStart(2, "0")}
+ / ${String(time.getHours()).padStart(2, "0")} : ${String(
+ time.getMinutes()
+ ).padStart(2, "0")}`;
+
+ return date;
+};
+
+/**
+ * ์๋ํฐ ํ
ํ๋ฆฟ
+ * @param {*} param0
+ * @returns
+ */
+const $editPost = ({ title, content, createdAt, updatedAt }) => {
+ content = content === null ? "" : content;
+ return `
+
+
+ ์์ฑ๋ ์ง : ${formatTime(new Date(createdAt))}
+ ์์ ๋ ์ง : ${formatTime(new Date(updatedAt))}
+
+
+ `;
+};
+
+/**
+ * ๋ฆฌ์คํธ ํ
ํ๋ฆฟ
+ * @param {*} posts
+ * @returns
+ */
+const $listContent = (post, selectedPostId, openedId) => {
+ let isIncluded = false;
+ if (openedId.length > 0 && openedId !== undefined) {
+ isIncluded = openedId.includes(post.id.toString());
+ }
+ return `
+
+
+
+
+ ${post.title}
+
+
+
+
+
+
+
+ `;
+};
+
+/**
+ * ์์ ๋ฆฌ์คํธ ์ถ๋ ฅ
+ * @param {*} posts
+ * @returns
+ */
+const $onLoadChildList = (posts, selectedPostId, openedId, hasIncluded) => {
+ if (Array.isArray(posts)) {
+ return `
+
+ ${posts
+ .map((post) => {
+ const isChildIncluded = openedId.includes(post.id.toString());
+ return `
+ ${$listContent(post, selectedPostId, openedId)}
+ ${
+ post.documents.length > 0
+ ? `${$onLoadChildList(
+ post.documents,
+ selectedPostId,
+ openedId,
+ isChildIncluded
+ )}`
+ : `${$emptyPage(isChildIncluded)}`
+ }
+ `;
+ })
+ .join("")}
+
+ `;
+ }
+};
+
+/**
+ * ๋ถ๋ชจ ๋ฆฌ์คํธ ์ ์ฒด ์ถ๋ ฅ
+ * @param {*} posts
+ * @returns
+ */
+const $onLoadParentList = (posts, selectedPostId, openedId) => {
+ if (Array.isArray(posts)) {
+ return `
+
+ ${posts
+ .map((post) => {
+ const isParentIncluded = openedId.includes(post.id.toString());
+ return `
+ -
+ ${$listContent(post, selectedPostId, openedId)}
+ ${
+ post.documents.length > 0
+ ? `${$onLoadChildList(
+ post.documents,
+ selectedPostId,
+ openedId,
+ isParentIncluded
+ )}`
+ : `${$emptyPage(isParentIncluded)}`
+ }
+
+ `;
+ })
+ .join("")}
+
+ `;
+ }
+};
+
+/**
+ * ํฌ์คํธ ๋ฆฌ์คํธ ํค๋
+ * @returns
+ */
+const $postsPageHeader = () => {
+ return `
+
+ `;
+};
+
+/**
+ * ํ์ ํ์ด์ง ์์
+ * @returns
+ */
+const $emptyPage = (hasIncluded) => {
+ return `
+
+ ํ์ ํ์ด์ง๊ฐ ์์ต๋๋ค.
+
+ `;
+};
+
+/**
+ * ํ์ด์ง ์ถ๊ฐ ๋ฒํผ
+ * @returns
+ */
+const $createPostBtn = () => {
+ return `
+
+
+
+ `;
+};
+
+/**
+ * ํฌ์คํธ ์์ฑ ๋ชจ๋ฌ์ฐฝ
+ * @returns html
+ *
+ */
+const $createPostModal = () => {
+ return `
+
+
+
+
+
๐์ค๋ํ๋ฃจ ์ด๋ ๋์?
+
+
+
+
+
+
+
+
+ `;
+};
+
+export {
+ $home,
+ $editPost,
+ $postsPageHeader,
+ $onLoadParentList,
+ $emptyPage,
+ $createPostBtn,
+ $createPostModal,
+};
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 00000000..98c2c171
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,3 @@
+{
+ "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }]
+}