diff --git a/README.assets/yenotion.png b/README.assets/yenotion.png
new file mode 100644
index 00000000..2f3bc3c2
Binary files /dev/null and b/README.assets/yenotion.png differ
diff --git a/README.assets/yenotion_wireframe (1).png b/README.assets/yenotion_wireframe (1).png
new file mode 100644
index 00000000..000b4978
Binary files /dev/null and b/README.assets/yenotion_wireframe (1).png differ
diff --git a/README.md b/README.md
index 82b85e81..46b9e4ce 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,87 @@
-# ๐ 4์ฃผ์ฐจ ํ๋ก์ ํธ[Project1]
+# ๐YENOTION
-## ํ์ ํ๋ก์ ํธ
-- ํ๋ก์ ํธ ๊ธฐํ
- - ํ๋ก์ ํธ ์ํ ๊ธฐ๊ฐ : 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 [ํ๋ก์ ํธ] ๋
ธ์
ํด๋ก๋ ์๊ตฌ์ฌํญ** ํ์ธ ๋ถํ๋๋ฆฝ๋๋ค.
+
+## ๐ธ ํ๋ก์ ํธ ์๊ฐ ๋ฐ ์ํ ๊ธฐ๊ฐ
+
+* API๋ฅผ ํ์ฉํ์ฌ ๋ฌธ์ ์๋ ์ ์ฅ ํธ์ง๊ธฐ๋ฅผ ๊ตฌํํฉ๋๋ค. ํ์ ๊ธฐ๋ฅ์ ๊ตฌํ ํ ์ถ๊ฐ ๊ธฐ๋ฅ์ ์์ ๋กญ๊ฒ ๊ตฌํํฉ๋๋ค.
+
+> - ํ๋ก์ ํธ ์ํ ๊ธฐ๊ฐ : 2022๋
11์ 8์ผ(ํ) ~ 2022๋
11์ 16์ผ(์)
+> - ๋ฉํฐ ์ฝ๋ ๋ฆฌ๋ทฐ ๊ธฐ๊ฐ : 2022๋
11์ 17์ผ(๋ชฉ) ~ 2022๋
11์ 20์ผ(์ผ)
+> - ๋ฉํ ์ฝ๋ ๋ฆฌ๋ทฐ ๊ธฐ๊ฐ : 2022๋
11์ 17์ผ(๋ชฉ) ~ 2022๋
11์ 22์ผ(ํ)
+> - ์ฝ๋ ๋ฆฌ๋ทฐ ๋ฐ์ ๊ธฐ๊ฐ : 2022๋
11์ 23์ผ(์) ~ 2022๋
11์ 25์ผ(๊ธ)
+
+## ๐ธ ๊ตฌ์กฐ ์ค๊ณ
+
+### ์์ด์ดํ๋ ์
+
+
+
+
+
+### ์ปดํฌ๋ํธ ๊ตฌ์กฐ
+
+
+
+## ๐ธ ๊ธฐ๋ฅ ๋ช
์ธ
+
+### ๊ธฐ๋ณธ ๊ธฐ๋ฅ
+
+- [x] ๋ฉ์ธ ํ์ด์ง ์ผ์ชฝ์ ์ ์ฒด ๋ฌธ์ ํธ๋ฆฌ๋ฅผ ์ถ๋ ฅํฉ๋๋ค.
+ - ๋ฃจํธ ๋ฌธ์๋ฅผ ํด๋ฆญํ๋ฉด ์ค๋ฅธ์ชฝ ํธ์ง๊ธฐ ์์ญ์ ๋ฌธ์ ๋ด์ฉ์ ๋ ๋๋งํฉ๋๋ค.
+ - ๋ฌธ์ ์ด๋ฆ์ด ํน์ ๊ธธ์ด๋ฅผ ๋์ผ๋ฉด ํน์ ๊ธธ์ด๊น์ง ์๋ฅด๊ณ ... ์๋ต์ผ๋ก ํ์ํ๋ค.
+
+- [x] ์๋ก์ด ๋ฌธ์๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
+ - ๊ฐ ๋ฌธ์์ ์ฐ์ธก์ + ๋ฒํผ์ด ์๊ณ , ํด๋น ๋ฒํผ์ ํด๋ฆญํ๋ฉด ํ์์ ์๋ก์ด ๋ฌธ์ ์์ฑ๋๊ณ ํธ์ง ํ๋ฉด์ผ๋ก ๋์ด๊ฐ๋๋ค.
+ - ๋ฃจํธ ๋ฌธ์ ๋๋ ํน์ ๋ฌธ์ ํ์์ ์๋ก์ด ๋ฌธ์๋ฅผ ์ถ๊ฐํ ์ ์์ต๋๋ค.
+
+- [x] ๋ฌธ์๋ฅผ ์์ ํ๊ณ ์ ์ฅํ ์ ์์ต๋๋ค.
+ - ๋ฌธ์ ์ ์ฅ ๋ฒํผ์ ๋ฐ๋ก ๋ง๋ค์ง ์์ต๋๋ค.
+ - document save api๋ฅผ ์ด์ฉ, ์๋ฒ์ ์ง์์ ์ธ ์์ฒญ์ ํตํด ์ ์ฅ๋๋๋ก ์ค์ ํฉ๋๋ค.
+- ๋ฌธ์์ ๋ด์ฉ์ ๋งํฌ๋ค์ด ๋ฌธ๋ฒ์ ์ฌ์ฉํ์ฌ ๋ํ๋ผ ์ ์์ต๋๋ค.
+
+ - ์ค๋ฅธํธ ๋ฌธ์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์์ ์ค์ ํ์ด์ง์ ๋ด์ฉ์ ํ์ธํ ์ ์์ต๋๋ค.
+
+- [x] ๋ฌธ์๋ฅผ ์ญ์ ํ ์ ์์ต๋๋ค.
+
+- [x] History API๋ฅผ ํ์ฉํ์ฌ SPA๋ก ์ ์ํฉ๋๋ค.
+ - ๋ฃจํธ URL: ๋ฌธ์ ์ ํ์ด ์์ง ์ ๋ ์ํ๋ฅผ ๋ณด์ฌ์ค๋๋ค.
+ * ํธ์ง๊ธฐ์๋ ๊ธฐ๋ณธ ํ์ด์ง ์ถ๋ ฅ
+ - /documents/{documentId}: ํด๋น ๋ฌธ์์ ๋ด์ฉ์ ๋ถ๋ฌ์ ํธ์ง๊ธฐ์ ์ถ๋ ฅ
+
+
+
+### ์ถ๊ฐ ๊ธฐ๋ฅ
+
+[๊ณผ์ ์ ์]
+
+- [x] ๊ธฐ๋ณธ์ ์ผ๋ก ํธ์ง๊ธฐ๋ textarea ๊ธฐ๋ฐ์ผ๋ก ๋จ์ํ ํ
์คํธ ํธ์ง๊ธฐ๋ก ์์ํ๋, ์ฌ๋ ฅ์ด ๋๋ฉด div์ contentEditable์ ์กฐํฉํด์ ์ข ๋ Richํ ์๋ํฐ๋ฅผ ๋ง๋ค์ด๋ด
๋๋ค.
+
+- [x] ํธ์ง๊ธฐ ์ตํ๋จ์๋ ํ์ฌ ํธ์ง ์ค์ธ ๋ฌธ์์ ํ์ ๋ฌธ์๋ค์ ๋งํฌ๋ฅผ ์ถ๊ฐ.
+
+- [ ] `ํ์ด์ง ๋ฉ์
`: ํธ์ง๊ธฐ ๋ด์์ @์์ํด์ ๋ค๋ฅธ ๋ฌธ์์ title์ ์ ์ ๊ฒฝ์ฐ, ์๋์ผ๋ก ํด๋น Document์ ํธ์ง ํ์ด์ง๋ก ์ด๋ํ๋ ๋งํฌ๋ฅผ ๊ฑฐ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํฉ๋๋ค.
+
+[๊ธฐํ ์์ด๋์ด ์ ๋ฆฌ]
+
+* ๋งํฌ๋ค์ด ํธ์ง๊ธฐ ๋ง๋ค๊ธฐ
+* ๊ฒ์ ๊ธฐ๋ฅ
+* ๋คํฌ๋ชจ๋
+* ์๋ฒ ๋
+* ๋ถ๋งํฌ
+* ํ์ด๋ผ์ดํ
+* ์ด๋ฏธ์ง ์๋ฒ ๋ฉ
+* ๊ณต์ ํ๊ธฐ
+* undo, redo
+* ์์ ์ด๋ ฅ ์ถ๋ ฅ
+* ๊ณผ๊ฑฐ ์ํ๋ก ๋์๊ฐ๊ธฐ
+
+
+
+## ๐ธ๋๋์
+
+* ๊ณํ์ ๊ฑฐ์ฐฝํ๊ฒ ์ธ์ฐ๊ณ ์์ํ์ง๋ง, ๊ฒฐ๋ก ์ ์ผ๋ก ์ถ๊ฐ ๊ธฐ๋ฅ์ ๋ง์ด ๊ตฌํํ์ง ๋ชปํ๋ค. ์ผ์ฃผ์ผ ์จ์ ํ ํ๋ก์ ํธ๋ฅผ ์ํํ์ผ๋ฉด ์ง๋๋ฅผ ๋ ๋ง์ด ๋๊ฐ ์ ์์๊ฒ ์ง๋ง,, ๊ทธ๋๋ ๋ค์ ๊ฐ๋ฐ ์ผ์ ์ฐ์ ์ ๋ ์ํด๋ณด์!
+* SPA๋ก ๊ฐ๋ฐ ์ ํ์ด์ง ์๋ก๊ณ ์นจ ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ์ -> express ๊ฐ์ ์๋ฒ๋ฅผ ๋ฐ๋ก ๋๋ ๊ฒ์ด ํ์ํ ๊ฒ ๊ฐ๋ค.
+* ์ด๋ฒ ํ๋ก์ ํธ์์ API ์ฃผ์๋ฅผ ๊ฐ๋ฆฌ๊ธฐ ์ํด API ์ฃผ์๋ฅผ ๋ด์ ํ์ผ์ gitignore๋ก ์ค์ ํ์ฌ git์ ์ฌ๋ผ๊ฐ์ง ์๋๋ก ์ฒ๋ฆฌํ์ง๋ง -> ์ด๋ฐ ๊ฒฝ์ฐ ์ด๋ป๊ฒ ๋ฐฐํฌ๋ฅผ ํ ์ ์์์ง์ ๊ฐ์ ๊ณ ๋ฏผ๊ณผ, ํ๊ฒฝ ๋ณ์๋ก ์ ์ํด์ ์ฌ์ฉํ์ผ๋ฉด ๋ ์ข์์ ๊ฑฐ ๊ฐ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
+* API ๋น๋๊ธฐ ํต์ ๋ก์ง์ ๋ฐ๋ก utils ํจ์๋ก ๋ถ๋ฆฌํด์ฃผ์๋๋ฐ, ํจ์๋ฅผ ๊ณ์ ๋ถ๋ฆฌํ๋ค ๋ณด๋ API ์์ฒญํ๊ณ ์๋ต ๋ฐ์์จ ํ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๋ ๋ถ๋ถ์์ ์ด์ง ๋ฒ๋ฒ
์๋ค.. async await ๊ตฌ๋ฌธ์ ์ฌ์ฉํ ๋ ๋ฐํ๊ฐ์ด promise์ด๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ํ์ ์ปดํฌ๋ํธ๋ก ๋๊ธฐ์ง ์๊ณ -> setState๋ก ๋ถ๋ชจ์ ์ ์ฅํ๊ณ ์ดํ ํ์ ์ปดํฌ๋ํธ์ ๊ฐ์ ์ ๋ฌํ๋ค. -> ์ฒ์์ ๋น๋๊ธฐ ํต์ ํ๋ ์ผ๋ จ์ ๊ณผ์ (๋น๋๊ธฐ ์์ฒญ, ์๋ต ํ ์ํ๊ฐ ๋ณ๊ฒฝ ํ ๋ฆฌ๋ ๋๋ง)์ด ์ ์๋ผ์ ๊ผผ๊ผผํ ํ์ธํ๋ฉด์ ์์
ํ๋ค.
+* ๋ฐ๋๋ผ ์๋ฐ์คํฌ๋ฆฝํธ๋ก ์์
ํ๋ ๊ฑด ์ฌ๋ฐ๊ฒ ๋๊ปด์ง๋๋ฐ, css์์ class๋ช
๊ด๋ฆฌ ๋ฐ ์คํ์ผ ์์ฑ ์ฌ์ฌ์ฉ, ์ํ๊ฐ ๊ด๋ฆฌ, ๋ฆฌ๋ ๋๋ง ๋ฑ ์ ๊ฒฝ์จ์ผ ํ๋ ๊ฒ๋ค์ด ๋ง์์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ํ๋ ์์ํฌ ์ฌ์ฉ์ด ํจ์ฌ ํธํ๊ธด ํ๋ค.
+
diff --git a/_redirects b/_redirects
new file mode 100644
index 00000000..7797f7c6
--- /dev/null
+++ b/_redirects
@@ -0,0 +1 @@
+/* /index.html 200
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..c02ecd07
--- /dev/null
+++ b/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+ YENOTION
+
+
+
+
+
+
+
+
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 00000000..38d74ddf
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,9 @@
+import Header from "./components/Header.js";
+import DocumentEditPage from "./pages/DocumentEditPage.js";
+
+function App({ $target }) {
+ new Header({ $target });
+ new DocumentEditPage({ $target });
+}
+
+export default App;
diff --git a/src/apis/baseUrl.js b/src/apis/baseUrl.js
new file mode 100644
index 00000000..6a034320
--- /dev/null
+++ b/src/apis/baseUrl.js
@@ -0,0 +1,6 @@
+export const API_END_POINT = "https://kdt-frontend.programmers.co.kr";
+
+export const HEADERS = {
+ "x-username": "yezyvibe",
+ "Content-Type": "application/json",
+};
diff --git a/src/apis/documentApi.js b/src/apis/documentApi.js
new file mode 100644
index 00000000..1eb8caa6
--- /dev/null
+++ b/src/apis/documentApi.js
@@ -0,0 +1,26 @@
+import { request } from "./index.js";
+
+export const getRootDocuments = async () => {
+ const response = await request("/documents");
+ return response;
+};
+
+export const createNewDocument = async (data) => {
+ const response = await request("/documents", "POST", data);
+ return response;
+};
+
+export const getDocumentDetail = async (documentId) => {
+ const response = await request(`/documents/${documentId}`);
+ return response;
+};
+
+export const updateDocument = async (documentId, data) => {
+ const response = await request(`/documents/${documentId}`, "PUT", data);
+ return response;
+};
+
+export const deleteDocument = async (documentId) => {
+ const response = await request(`/documents/${documentId}`, "DELETE");
+ return response;
+};
diff --git a/src/apis/index.js b/src/apis/index.js
new file mode 100644
index 00000000..153b606d
--- /dev/null
+++ b/src/apis/index.js
@@ -0,0 +1,20 @@
+import { API_END_POINT, HEADERS } from "./baseUrl.js";
+
+export const request = async (url, method = "GET", data, defaultValue = []) => {
+ try {
+ const response = await fetch(API_END_POINT + url, {
+ method,
+ headers: HEADERS,
+ body: JSON.stringify(data),
+ });
+
+ if (response.ok) {
+ const json = await response.json();
+ return json;
+ }
+
+ return defaultValue;
+ } catch (e) {
+ console.log(e.message);
+ }
+};
diff --git a/src/components/DocumentEditor.js b/src/components/DocumentEditor.js
new file mode 100644
index 00000000..73d76491
--- /dev/null
+++ b/src/components/DocumentEditor.js
@@ -0,0 +1,90 @@
+import { navigate } from "../utils/router.js";
+import EditorPreview from "./EditorPreview.js";
+import { className } from "../utils/constants.js";
+import { debounce } from "../utils/debounce.js";
+
+function DocumentEditor({ $target, initialState, onChange }) {
+ const { editorDocumentTitle, editorDocumentContent } = className;
+
+ const $editor = document.createElement("section");
+ $editor.className = "document-editor";
+ $target.appendChild($editor);
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ editorPreview.setState({
+ data: this.state.data,
+ });
+ };
+
+ const editorPreview = new EditorPreview({
+ $target,
+ initialState: {
+ data: this.state.data,
+ },
+ });
+
+ const render = () => {
+ const { title, content, documents } = this.state.data;
+
+ if (!title) {
+ $editor.innerHTML = `
+ loading..
+ `;
+ return;
+ }
+
+ $editor.innerHTML = `
+
+
+ ${
+ content ? content : ""
+ }
+
+ ${
+ documents.length > 0
+ ? `
+
๐ํ์ ๋ฌธ์ ๋ชฉ๋ก
+ ${documents
+ .map(({ id, title }) => `
${title}`)
+ .join("")}
+
`
+ : ""
+ }
+ `;
+ };
+
+ render();
+
+ const updateDocument = () => {
+ const $title = document.querySelector(`.${editorDocumentTitle}`);
+ const $content = document.querySelector(`.${editorDocumentContent}`);
+
+ onChange($title.value, $content.innerText);
+ };
+
+ $editor.addEventListener("keyup", (e) => {
+ const $div = e.target.closest("div");
+
+ if ($div) {
+ debounce(updateDocument, 500);
+ }
+ });
+
+ $editor.addEventListener("click", (e) => {
+ e.preventDefault();
+
+ const $a = e.target.closest("a");
+
+ if ($a) {
+ const [, , , , documentId] = $a.href.split("/");
+ navigate(`/documents/${documentId}`);
+ }
+ });
+}
+
+export default DocumentEditor;
diff --git a/src/components/DocumentNode.js b/src/components/DocumentNode.js
new file mode 100644
index 00000000..4e463707
--- /dev/null
+++ b/src/components/DocumentNode.js
@@ -0,0 +1,112 @@
+import { navigate } from "../utils/router.js";
+import { className } from "../utils/constants.js";
+
+function DocumentNode({ $target, initialState, onCreate, onDelete }) {
+ const $node = document.createElement("article");
+ $target.appendChild($node);
+
+ const { addButton, deleteButton } = className;
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ this.render();
+ };
+
+ this.render = () => {
+ const { data } = this.state;
+
+ $node.innerHTML = `
+
+ `;
+ };
+
+ this.render();
+
+ const onClickButton = ($button) => {
+ const {
+ className,
+ dataset: { parentId },
+ } = $button;
+
+ if (!parentId) {
+ return;
+ }
+
+ if (className === addButton) {
+ onCreate(parseInt(parentId));
+ return;
+ }
+
+ if (className === deleteButton) {
+ onDelete(parentId);
+ return;
+ }
+ };
+
+ const onSelectDocument = ($li) => {
+ const { documentId } = $li.dataset;
+
+ if (documentId) {
+ navigate(`/documents/${documentId}`);
+ }
+ };
+
+ $node.addEventListener("click", (e) => {
+ const $button = e.target.closest("button");
+ if ($button) {
+ onClickButton($button);
+ return;
+ }
+
+ const $li = e.target.closest("li");
+ if ($li) {
+ onSelectDocument($li);
+ return;
+ }
+ });
+}
+
+export default DocumentNode;
diff --git a/src/components/DocumentTree.js b/src/components/DocumentTree.js
new file mode 100644
index 00000000..027f383b
--- /dev/null
+++ b/src/components/DocumentTree.js
@@ -0,0 +1,105 @@
+import {
+ createNewDocument,
+ deleteDocument,
+ getRootDocuments,
+} from "../apis/documentApi.js";
+import DocumentNode from "./DocumentNode.js";
+import { className } from "../utils/constants.js";
+import { messages } from "../utils/messages.js";
+import { navigate } from "../utils/router.js";
+import { getDocumentIdFromPathname } from "../utils/documentId.js";
+
+function DocumentTree({ $target, initialState }) {
+ const $tree = document.createElement("section");
+ $target.appendChild($tree);
+
+ const { treeRootAddButton } = className;
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ this.render();
+ };
+
+ const checkDocumentIdAndRoute = (documentId) => {
+ if (getDocumentIdFromPathname() === documentId) {
+ navigate("/");
+ }
+ };
+
+ this.render = () => {
+ $tree.innerHTML = `
+
+ add
+ ์๋ก์ด ๋ฌธ์
+
+ `;
+
+ new DocumentNode({
+ $target: $tree,
+ initialState: {
+ data: this.state.data,
+ },
+ onCreate: (parentId) => {
+ this.createDocument(parentId);
+ },
+ onDelete: async (documentId) => {
+ const answer = confirm(messages.askDelete);
+ if (!answer) return;
+
+ await deleteDocument(documentId);
+ this.getData();
+
+ checkDocumentIdAndRoute(documentId);
+ },
+ });
+ };
+
+ this.getData = async () => {
+ const data = await getRootDocuments();
+ this.setState({
+ ...this.state,
+ data,
+ });
+ };
+
+ this.createDocument = async (parentId) => {
+ const title = prompt(messages.askNewTitle);
+
+ if (title === null) {
+ return;
+ }
+
+ if (!title.trim()) {
+ alert(messages.invalidTitle);
+ return;
+ }
+
+ const newData = {
+ title: title.trim(),
+ parent: parentId,
+ };
+
+ const { id } = await createNewDocument(newData);
+ navigate(`/documents/${id}`);
+
+ this.getData();
+ };
+
+ this.init = () => {
+ this.getData();
+ };
+
+ this.init();
+
+ $tree.addEventListener("click", (e) => {
+ const $div = e.target.closest("div");
+
+ if ($div?.className === treeRootAddButton) {
+ this.createDocument(null);
+ }
+ });
+}
+
+export default DocumentTree;
diff --git a/src/components/EditorPreview.js b/src/components/EditorPreview.js
new file mode 100644
index 00000000..414c2259
--- /dev/null
+++ b/src/components/EditorPreview.js
@@ -0,0 +1,31 @@
+import { parseMarkdown } from "../utils/parseMarkdown.js";
+
+function EditorPreview({ $target, initialState }) {
+ const $preview = document.createElement("section");
+ $preview.className = "editor-preview";
+ $target.appendChild($preview);
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ this.render();
+ };
+
+ this.render = () => {
+ if (!this.state.data) return;
+
+ const { content } = this.state.data;
+
+ $preview.innerHTML = `
+ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
+ ${
+ content ? parseMarkdown(content) : "๋ด์ฉ ์์"
+ }
+ `;
+ };
+
+ this.render();
+}
+
+export default EditorPreview;
diff --git a/src/components/Header.js b/src/components/Header.js
new file mode 100644
index 00000000..ffb77d4b
--- /dev/null
+++ b/src/components/Header.js
@@ -0,0 +1,16 @@
+function Header({ $target }) {
+ const $title = document.createElement("div");
+ $title.className = "header-title";
+ $target.appendChild($title);
+
+ this.render = () => {
+ $title.innerHTML = `
+
+ YENOTION
+ `;
+ };
+
+ this.render();
+}
+
+export default Header;
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 00000000..595e10ef
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,3 @@
+import App from "./App.js";
+
+new App({ $target: document.querySelector("#App") });
diff --git a/src/pages/DocumentEditPage.js b/src/pages/DocumentEditPage.js
new file mode 100644
index 00000000..5a78774f
--- /dev/null
+++ b/src/pages/DocumentEditPage.js
@@ -0,0 +1,88 @@
+import DocumentEditor from "../components/DocumentEditor.js";
+import DocumentTree from "../components/DocumentTree.js";
+import { getDocumentDetail, updateDocument } from "../apis/documentApi.js";
+import { handleLocationChange } from "../utils/router.js";
+import { getDocumentIdFromPathname } from "../utils/documentId.js";
+
+function DocumentEditPage({ $target }) {
+ const $editPage = document.createElement("div");
+ $editPage.className = "main-content";
+ $target.appendChild($editPage);
+
+ const $tree = document.createElement("section");
+ $tree.className = "document-tree";
+ $editPage.appendChild($tree);
+
+ const documentTree = new DocumentTree({ $target: $tree });
+
+ const $editor = document.createElement("section");
+ $editor.className = "document-editor-wrapper";
+ $editPage.appendChild($editor);
+
+ this.state = {
+ data: {},
+ };
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ };
+
+ this.route = async () => {
+ const { pathname } = window.location;
+ const documentId = getDocumentIdFromPathname();
+
+ if (pathname === "/") {
+ $editor.innerHTML = "";
+ return;
+ }
+
+ if (documentId) {
+ $editor.innerHTML = "";
+
+ const data = await getDocumentDetail(documentId);
+ this.setState({
+ data,
+ });
+
+ this.documentEditor = new DocumentEditor({
+ $target: $editor,
+ initialState: {
+ data: this.state.data,
+ },
+ onChange: (newTitle, newContent) => {
+ this.updateData(documentId, {
+ title: newTitle,
+ content: newContent,
+ });
+ },
+ });
+ }
+ };
+
+ this.updateData = async (documentId, data) => {
+ await updateDocument(documentId, data);
+
+ const newData = await getDocumentDetail(documentId);
+
+ this.documentEditor.setState({
+ data: newData,
+ });
+
+ documentTree.getData();
+ };
+
+ this.init = () => {
+ window.addEventListener("onChangeLocation", (e) =>
+ handleLocationChange(e, this.route)
+ );
+ window.addEventListener("popstate", () => {
+ this.route();
+ });
+
+ this.route();
+ };
+
+ this.init();
+}
+
+export default DocumentEditPage;
diff --git a/src/styles/style.css b/src/styles/style.css
new file mode 100644
index 00000000..d385c0cb
--- /dev/null
+++ b/src/styles/style.css
@@ -0,0 +1,168 @@
+body {
+ margin: 0;
+ padding: 0;
+ font-family: "SUIT-Medium";
+}
+
+@font-face {
+ font-family: "SUIT-Medium";
+ src: url("https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_suit@1.0/SUIT-Medium.woff2")
+ format("woff2");
+ font-weight: normal;
+ font-style: normal;
+}
+
+.main-content {
+ display: flex;
+ width: 100vw;
+ height: 100vh;
+}
+
+.header-title {
+ border-bottom: 1px solid rgb(235, 235, 235);
+ padding-left: 16px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background-color: lightgoldenrodyellow;
+}
+
+.header-title img {
+ width: 50px;
+ height: 50px;
+}
+
+.document-editor-wrapper {
+ display: flex;
+ width: 100vw;
+}
+
+.document-editor {
+ min-width: 40vw;
+ max-width: 40vw;
+}
+
+.document-tree {
+ background-color: rgb(251, 251, 250);
+ min-width: 250px;
+ max-width: 300px;
+ overflow: scroll;
+}
+
+.tree-node {
+ padding-left: 12px;
+ border-left: 1px solid lightgray;
+}
+
+.node-item {
+ list-style: none;
+ margin-top: 6px;
+ cursor: pointer;
+}
+
+.tree-node-li {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ max-width: 300px;
+}
+
+.tree-add-button-wrapper {
+ display: flex;
+ padding: 16px 10px;
+ cursor: pointer;
+ align-items: center;
+}
+
+.node-title {
+ display: flex;
+ align-items: center;
+}
+
+.node-title span {
+ max-width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.node-button-group {
+ display: flex;
+}
+
+.add-button,
+.delete-button {
+ all: unset;
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ text-align: center;
+ border-radius: 4px;
+}
+
+.add-button:hover,
+.delete-button:hover {
+ background-color: lightgray;
+}
+
+.add-button span,
+.delete-button span {
+ font-size: small;
+ color: black;
+}
+
+.eidtor-wrapper {
+ display: flex;
+}
+
+.editor-document-content {
+ height: 70vh;
+ width: 100%;
+ font-size: 20px;
+ padding: 32px;
+ white-space: pre-wrap;
+ overflow-x: auto;
+}
+
+.editor-document-content::-webkit-scrollbar {
+ display: none;
+}
+
+.document-editor {
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+}
+
+.editor-document-title {
+ height: 50px;
+ font-size: 32px;
+ font-weight: bold;
+ padding: 10px;
+ border: none;
+ font-family: SUIT-Medium;
+}
+
+.child-documents-wrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.modal {
+ display: none;
+}
+
+.editor-preview {
+ font-size: 20px;
+ padding: 16px;
+ border-left: 1px solid lightgray;
+}
+
+.editor-preview h1 {
+ font-size: 32px;
+}
+
+.preview-title {
+ margin: 0;
+ padding: 10px;
+}
diff --git a/src/utils/constants.js b/src/utils/constants.js
new file mode 100644
index 00000000..d30218cc
--- /dev/null
+++ b/src/utils/constants.js
@@ -0,0 +1,8 @@
+export const className = {
+ editorDocumentTitle: "editor-document-title",
+ editorDocumentContent: "editor-document-content",
+ treeRootAddButton: "tree-add-button-wrapper",
+
+ addButton: "add-button",
+ deleteButton: "delete-button",
+};
diff --git a/src/utils/debounce.js b/src/utils/debounce.js
new file mode 100644
index 00000000..d748b372
--- /dev/null
+++ b/src/utils/debounce.js
@@ -0,0 +1,11 @@
+let timeoutId = null;
+
+export const debounce = (callback, delay) => {
+ if (timeoutId !== null) {
+ clearTimeout(timeoutId);
+ }
+
+ timeoutId = setTimeout(() => {
+ callback();
+ }, delay);
+};
diff --git a/src/utils/documentId.js b/src/utils/documentId.js
new file mode 100644
index 00000000..2af74ec2
--- /dev/null
+++ b/src/utils/documentId.js
@@ -0,0 +1,4 @@
+export const getDocumentIdFromPathname = () => {
+ const [, , currentDocumentId] = location.pathname.split("/");
+ return currentDocumentId;
+};
diff --git a/src/utils/messages.js b/src/utils/messages.js
new file mode 100644
index 00000000..0b4e0a82
--- /dev/null
+++ b/src/utils/messages.js
@@ -0,0 +1,5 @@
+export const messages = {
+ askDelete: "๋ฌธ์๋ฅผ ์ ๋ง ์ญ์ ํ์๊ฒ ์ต๋๊น?",
+ askNewTitle: "์๋ก์ด ๋ฌธ์์ ์ ๋ชฉ์ ์
๋ ฅํด์ฃผ์ธ์.",
+ invalidTitle: "์ฌ๋ฐ๋ฅธ ์ ๋ชฉ์ ์
๋ ฅํด์ฃผ์ธ์.",
+};
diff --git a/src/utils/parseMarkdown.js b/src/utils/parseMarkdown.js
new file mode 100644
index 00000000..0e2a93bd
--- /dev/null
+++ b/src/utils/parseMarkdown.js
@@ -0,0 +1,62 @@
+export const parseMarkdown = (text) => {
+ //ul
+ text = text.replace(/^\s*\n\*/gm, "\n*");
+ text = text.replace(/^(\*.+)\s*\n([^\*])/gm, "$1\n
\n\n$2");
+ text = text.replace(/^\*(.+)/gm, "$1");
+
+ //ol
+ text = text.replace(/^\s*\n\d\./gm, "\n1.");
+ text = text.replace(/^(\d\..+)\s*\n([^\d\.])/gm, "$1\n
\n\n$2");
+ text = text.replace(/^\d\.(.+)/gm, "$1");
+
+ //blockquote
+ text = text.replace(/^\>(.+)/gm, "$1
");
+
+ //h
+ text = text.replace(/[\#]{6}(.+)/g, "$1
");
+ text = text.replace(/[\#]{5}(.+)/g, "$1
");
+ text = text.replace(/[\#]{4}(.+)/g, "$1
");
+ text = text.replace(/[\#]{3}(.+)/g, "$1
");
+ text = text.replace(/[\#]{2}(.+)/g, "$1
");
+ text = text.replace(/[\#]{1}(.+)/g, "$1
");
+
+ //alt h
+ text = text.replace(/^(.+)\n\=+/gm, "$1
");
+ text = text.replace(/^(.+)\n\-+/gm, "$1
");
+
+ //images
+ text = text.replace(
+ /\!\[([^\]]+)\]\(([^\)]+)\)/g,
+ '
'
+ );
+
+ //links
+ text = text.replace(
+ /[\[]{1}([^\]]+)[\]]{1}[\(]{1}([^\)\"]+)(\"(.+)\")?[\)]{1}/g,
+ '$1'
+ );
+
+ //font styles
+ text = text.replace(/[\*\_]{2}([^\*\_]+)[\*\_]{2}/g, "$1");
+ text = text.replace(/[\*\_]{1}([^\*\_]+)[\*\_]{1}/g, "$1");
+ text = text.replace(/[\~]{2}([^\~]+)[\~]{2}/g, "$1");
+
+ //pre
+ text = text.replace(/^\s*\n\`\`\`(([^\s]+))?/gm, '');
+ text = text.replace(/^\`\`\`\s*\n/gm, "
\n\n");
+
+ //code
+ text = text.replace(/[\`]{1}([^\`]+)[\`]{1}/g, "$1");
+
+ //p
+ text = text.replace(/^\s*(\n)?(.+)/gm, function (m) {
+ return /\<(\/)?(h\d|ul|ol|li|blockquote|pre|img)/.test(m)
+ ? m
+ : "" + m + "
";
+ });
+
+ //strip p from pre
+ text = text.replace(/(\)\s*\n\(.+)\<\/p\>/gm, "$1$2");
+
+ return text;
+};
diff --git a/src/utils/router.js b/src/utils/router.js
new file mode 100644
index 00000000..c57133e5
--- /dev/null
+++ b/src/utils/router.js
@@ -0,0 +1,19 @@
+export const handleLocationChange = (e, route) => {
+ const { href } = e.detail;
+ const { pathname } = window.location;
+
+ if (pathname === href) return;
+
+ window.history.pushState(null, null, href);
+ route();
+};
+
+export const navigate = (url) => {
+ const locationChangeEvent = new CustomEvent("onChangeLocation", {
+ composed: true,
+ detail: {
+ href: url,
+ },
+ });
+ window.dispatchEvent(locationChangeEvent);
+};