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 = ` + + `; + 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; +}