diff --git a/asset/notion.png b/asset/notion.png
new file mode 100644
index 00000000..adcfb253
Binary files /dev/null and b/asset/notion.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..036211d8
--- /dev/null
+++ b/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+ 유진의 Notion
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 00000000..c930e0c9
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,30 @@
+import { EditorPage } from "./EditorPage.js";
+import { DocumentPage } from "./DocumentPage.js";
+import { initRouter } from "./router.js";
+
+export default function App ($target) {
+ const $app = document.createElement('div');
+ $app.className = 'mainApp'
+ const editorPage = new EditorPage($app);
+ const documentPage = new DocumentPage($app);
+
+ $target.appendChild($app);
+
+ this.route = () => {
+ const { pathname } = window.location;
+ if(pathname.includes('/documents/')){
+ const [, , documentId] = pathname.split('/');
+ editorPage.setState({...editorPage.state, id : documentId});
+ }
+ }
+ this.render = () => {
+ documentPage.render();
+ const { pathname } = window.location;
+ const [, , documentId] = pathname.split('/');
+ if(documentId){
+ editorPage.setState({...editorPage.state, id: documentId})
+ }
+ }
+ this.render()
+ initRouter({onRoute : this.route});
+}
\ No newline at end of file
diff --git a/src/Components/ChildDocuments.js b/src/Components/ChildDocuments.js
new file mode 100644
index 00000000..d113bb31
--- /dev/null
+++ b/src/Components/ChildDocuments.js
@@ -0,0 +1,33 @@
+import { routerNav } from '../router.js';
+
+export default function ChildDocument ({$target, $document}){
+ const $childDocument = document.createElement('div');
+ $childDocument.className = 'childLink';
+ $target.append($childDocument);
+
+ this.state = {
+ data : $document
+ }
+ this.setState = nextState => {
+ this.state = nextState
+ this.render();
+ }
+ this.render = () => {
+ if(this.state.data){
+ const { documents } = this.state.data
+ if(documents){
+ $childDocument.innerHTML = `${
+ documents.map(({id, title}) =>
+ `${title}`
+ ).join('')}`
+ }
+ }
+ }
+
+ this.render();
+ $childDocument.addEventListener('click', (e) => {
+ const $link = e.target.closest('li');
+ const { id } = $link.dataset;
+ routerNav(`/documents/${id}`)
+ })
+}
\ No newline at end of file
diff --git a/src/Components/DocumentCreate.js b/src/Components/DocumentCreate.js
new file mode 100644
index 00000000..ee68f616
--- /dev/null
+++ b/src/Components/DocumentCreate.js
@@ -0,0 +1,34 @@
+import { DocumentModal } from "./DocumentModal.js";
+
+export function DocumentCreate({$target, parentId, onSubmit}){
+ this.state = {
+ parentId :null,
+ }
+ this.setState = (nextState) => {
+ this.state = nextState;
+ }
+ this.render = () => {
+ const $createBtn = document.createElement('button');
+ $createBtn.className = 'createDoc';
+ if (parentId === null){
+ $createBtn.className = 'rootCreate'
+ }
+ $createBtn.textContent = '+';
+ $target.append($createBtn);
+ }
+ $target.addEventListener('click', (e) => {
+ const $createBtn = e.target.closest('button');
+ if($createBtn){
+ if($createBtn.classList.contains('rootCreate')){
+ e.stopImmediatePropagation();
+ const modal = new DocumentModal(null , onSubmit)
+ modal.modalOpen();
+ return
+ }
+ const { id } = $createBtn.nextElementSibling.dataset
+ const modal = new DocumentModal(id , onSubmit)
+ modal.modalOpen();
+ }
+ })
+ this.render()
+}
\ No newline at end of file
diff --git a/src/Components/DocumentDelete.js b/src/Components/DocumentDelete.js
new file mode 100644
index 00000000..a79c1ad0
--- /dev/null
+++ b/src/Components/DocumentDelete.js
@@ -0,0 +1,20 @@
+import { deleteDocuments } from "../api.js";
+import { routerNav } from "../router.js";
+
+export default function DocumentDelete ({$target, id}){
+ const $deleteBtn = document.createElement('button');
+ $deleteBtn.className= 'delete-btn'
+ $target.appendChild($deleteBtn)
+ $deleteBtn.textContent = '삭제하기'
+
+ this.state = { id }
+ this.setState = nextState => {
+ this.state = nextState
+ }
+ $deleteBtn.addEventListener('click', async (e) => {
+ await deleteDocuments(this.state.id)
+ alert("삭제 완료");
+ routerNav('/');
+ location.reload();
+ })
+}
\ No newline at end of file
diff --git a/src/Components/DocumentList.js b/src/Components/DocumentList.js
new file mode 100644
index 00000000..4434f766
--- /dev/null
+++ b/src/Components/DocumentList.js
@@ -0,0 +1,95 @@
+import { DocumentCreate } from "./DocumentCreate.js"
+import { routerNav } from '../router.js';
+
+export function DocumentList({$target, data =[], initialState, onSubmit}) {
+ let init = false;
+ this.state = initialState
+ this.setState = (nextState) => {
+ this.state = nextState
+ }
+ this.render = ($renderDOM = $target) => {
+ const $parentNode = document.createElement('div')
+ $parentNode.className = `doc-${this.state.selectedNode}`
+ $parentNode.style.marginLeft = `${this.state.depth * 10}px`;
+ const createBtn = new DocumentCreate({
+ $target: $parentNode,
+ parentId: this.state.parent,
+ onSubmit: onSubmit
+ })
+ const doc = document.createElement('li');
+ doc.setAttribute("data-id", `${data[0].id}`);
+ doc.setAttribute("class", 'doc');
+ doc.textContent =`${data[0].title}`
+ $parentNode.append(doc)
+ console.log(this.state.isOpen)
+ if(data[0].documents.length === 0){
+ const $haveNothing = document.createElement('div');
+ $haveNothing.classList.add('nothing')
+ $haveNothing.textContent = '하위 페이지 없음'
+ doc.append($haveNothing)
+ }
+ else {
+ data[0].documents.forEach((data => {
+ const documentList = new DocumentList({
+ $target: doc,
+ data: [data],
+ initialState: {parent: this.state.selectedNode, selectedNode: data.id, depth: this.state.depth + 1, isOpen: false},
+ onSubmit: onSubmit
+ })
+ documentList.render()
+ }))
+ }
+ init= true
+ $renderDOM.append($parentNode)
+
+ }
+
+ $target.addEventListener('click', (e) => {
+ e.stopPropagation();
+ if($target.classList.contains('documentPage') && !e.target.classList.contains('doc'))
+ return
+ else if (e.target.classList.contains('createDoc')){
+ return
+ }
+ else if (e.target.classList.contains('nothing')){
+ return
+ }
+ console.log(e.target)
+ console.log(this.state)
+ if(this.state.isOpen){
+ while(e.target.querySelector('div')){
+ e.target.classList.remove("open");
+ const $removeTarget = e.target.querySelector('div')
+ e.target.removeChild($removeTarget);
+ }
+ this.setState({
+ ...this.state,
+ isOpen: !this.state.isOpen,
+ })
+ return
+ }
+ const $li = e.target
+ if($li){
+ const { id } = $li.dataset;
+ if(data){
+ const childrenData = data.map(data => data.documents)
+ $li.classList.add('open')
+ if(childrenData[0].length > 0){
+ this.setState({
+ parent: this.state.depth === 0 ? parseInt(id, 10) : parseInt(this.state.parent, 10),
+ selectedNode: parseInt(id),
+ isOpen: !this.state.isOpen,
+ depth: this.state.depth
+ })
+ }
+ else {
+ this.setState({
+ ...this.state,
+ isOpen: !this.state.isOpen,
+ })
+ }
+ routerNav(`/documents/${id}`);
+ }
+ }
+ })
+}
\ No newline at end of file
diff --git a/src/Components/DocumentModal.js b/src/Components/DocumentModal.js
new file mode 100644
index 00000000..b8bb4e0a
--- /dev/null
+++ b/src/Components/DocumentModal.js
@@ -0,0 +1,46 @@
+export function DocumentModal(id , onSubmit){
+ const $app = document.querySelector('.app');
+ const $modalContainer = document.createElement('div')
+ $modalContainer.className = 'modal';
+ $app.appendChild($modalContainer);
+ this.render = () => {
+ $modalContainer.innerHTML = `
+
+
+
+
+ `
+ }
+
+ $modalContainer.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const $input = $modalContainer.querySelector('.modalText');
+ let content = $input.value;
+ if(content.length === 0) {
+ content = '제목 없음'
+ }
+ await onSubmit(content, id);
+ $input.value =''
+ alert('문서 생성이 완료되었습니다')
+ const modal = document.querySelector('.modal');
+ modal.remove()
+ })
+
+ $modalContainer.addEventListener('click', (e) => {
+ const $closeBtn = e.target.closest('button')
+ if(!$closeBtn) return
+ if ($closeBtn.classList.contains('closeBtn')){
+ const modal = document.querySelector('.modal');
+ modal.remove()
+ }
+ })
+
+ this.modalOpen = () => {
+ const modal = document.querySelector('.modal');
+ modal.style.display = "block";
+ document.body.style.overflow = "hidden";
+ }
+ this.render()
+}
\ No newline at end of file
diff --git a/src/Components/Editor.js b/src/Components/Editor.js
new file mode 100644
index 00000000..533e7e02
--- /dev/null
+++ b/src/Components/Editor.js
@@ -0,0 +1,40 @@
+export default function Editor({$target, initialState = {
+ title: '',
+ content: '',
+}, onEditing}){
+ const $editor = document.createElement('div');
+ $editor.className = 'editor'
+ $target.appendChild($editor);
+ this.state = initialState;
+ let isinitialize = false
+
+ this.setState = (nextState) => {
+ this.state = nextState
+ $editor.querySelector('[name=content]').value = this.state.content;
+ $editor.querySelector('[name=title]').value = this.state.title;
+ this.render();
+ }
+ this.render = () => {
+ if(!isinitialize){
+ $editor.innerHTML = `
+
+
+ `
+ isinitialize = true
+ }
+ }
+
+ this.render();
+ $editor.addEventListener('keyup', 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)
+ }
+ })
+}
\ No newline at end of file
diff --git a/src/DocumentPage.js b/src/DocumentPage.js
new file mode 100644
index 00000000..16724d82
--- /dev/null
+++ b/src/DocumentPage.js
@@ -0,0 +1,78 @@
+import { DocumentList } from "./Components/DocumentList.js";
+import { DocumentCreate } from "./Components/DocumentCreate.js";
+import { getDocuments, createDocuments } from "./api.js";
+import { routerNav } from "./router.js";
+
+export function DocumentPage ($target) {
+ const $documentPage = document.createElement('div');
+ $documentPage.className = 'documentPage'
+ $target.appendChild($documentPage);
+ const rootCreateBtn = new DocumentCreate({
+ $target: $documentPage,
+ parentId: null,
+ onSubmit: async (content, parent) => {
+ const createdData = await createDocuments(content, parent)
+ const { id } = createdData;
+ this.setState({...this.state, createdData})
+ await fetchDocuments();
+ routerNav(`/documents/${id}`);
+ }
+ })
+
+ this.state = {
+ documentData: []
+ }
+
+ this.setState = (nextState) => {
+ this.state = nextState
+ }
+
+ const fetchDocuments = async() => {
+ const documentData = await getDocuments();
+ this.setState({
+ ...this.state,
+ documentData
+ })
+ this.render()
+ }
+
+ this.render = () => {
+ $documentPage.innerHTML =''
+ rootCreateBtn.render();
+ if(Array.isArray(this.state.documentData) && this.state.documentData.length > 0){
+ this.state.documentData.forEach((data) => {
+ const $documentList = document.createElement('div');
+ $documentList.className = `root-${data.id}`
+ const createBtn = new DocumentCreate({
+ $target: $documentList,
+ parentId: data.id,
+ onSubmit: async (content, parent) => {
+ const createdData = await createDocuments(content, parent)
+ const { id } = createdData;
+ await fetchDocuments();
+ routerNav(`/documents/${id}`);
+ }
+ })
+ $documentPage.appendChild($documentList);
+ const documentList = new DocumentList({
+ $target: $documentList,
+ data: [data],
+ initialState:{
+ parent: data.id,
+ selectedNode: data.id,
+ isOpen: false,
+ depth: 1
+ },
+ onSubmit: async (content, parent) => {
+ const createdData = await createDocuments(content, parent);
+ const { id } = createdData;
+ await fetchDocuments();
+ routerNav(`/documents/${id}`);
+ }
+ })
+ documentList.render()
+ })
+ }
+ }
+ fetchDocuments();
+}
\ No newline at end of file
diff --git a/src/EditorPage.js b/src/EditorPage.js
new file mode 100644
index 00000000..305ee82f
--- /dev/null
+++ b/src/EditorPage.js
@@ -0,0 +1,71 @@
+import { editDocuments, getEditableDocuments } from './api.js';
+import ChildDocument from './Components/ChildDocuments.js';
+import Editor from './Components/Editor.js';
+import DocumentDelete from './Components/DocumentDelete.js';
+
+export function EditorPage($target) {
+ const $editorPage = document.createElement('div');
+ $editorPage.className = 'EditorPage'
+ let timer = null
+ this.state = {
+ id : null,
+ post: {
+ title: "",
+ content: "",
+ }
+ }
+ const childLink = new ChildDocument({
+ $target: $editorPage,
+ initialState: this.state.post
+ })
+ const editor = new Editor({
+ $target: $editorPage,
+ initialState: this.state.post,
+ onEditing: (newDocument) => {
+ if(timer !== null){
+ clearTimeout(timer);
+ }
+ timer = setTimeout(async () => {
+ editDocuments(newDocument, this.state.id)
+ }, 1000)
+ }
+ })
+
+ const documentDelete = new DocumentDelete({$target: $editorPage, id : this.state.id})
+ this.render = () => {
+ $target.appendChild($editorPage);
+ }
+ this.setState = async nextState => {
+ if (this.state.id !== nextState.id){
+ this.state = nextState
+ editor.setState(this.state.post || {
+ title:'',
+ content:''
+ });
+ documentDelete.setState({id: this.state.id})
+ childLink.setState({data: this.state.post} || {
+ title: '',
+ content:''
+ })
+ await fetchPost();
+ return
+ }
+ this.state = nextState
+ this.render()
+ editor.setState(this.state.post || {
+ title:'',
+ content:''
+ });
+ documentDelete.setState({id: this.state.id})
+ childLink.setState({data: this.state.post})
+ }
+
+ const fetchPost = async() => {
+ const { id } = this.state;
+ const post = await getEditableDocuments(id)
+ this.setState({
+ ...this.state,
+ post
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/api.js b/src/api.js
new file mode 100644
index 00000000..1d0bd2e7
--- /dev/null
+++ b/src/api.js
@@ -0,0 +1,37 @@
+import { request } from "./request.js"
+
+export const getDocuments = async() => {
+ const documentData = await request('/documents', {
+ method: "GET",
+ });
+ return documentData
+}
+
+export const getEditableDocuments = async(id) => {
+ const documentData = await request(`/documents/${id}`)
+ return documentData
+}
+
+export const createDocuments = async(content, parent) => {
+ const createdData = await request(`/documents`, {
+ method: "POST",
+ body: JSON.stringify({
+ title: content,
+ parent: parent,
+ })
+ })
+ return createdData
+}
+
+export const deleteDocuments = async(id) => {
+ await request(`/documents/${id}`, {
+ method: 'DELETE'
+ })
+}
+
+export const editDocuments = async(newDocument, id) => {
+ await request(`/documents/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(newDocument)
+ })
+}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 00000000..2c0b5d4c
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,5 @@
+import App from "./App.js";
+
+const $app = document.querySelector('.app');
+
+new App($app)
\ No newline at end of file
diff --git a/src/request.js b/src/request.js
new file mode 100644
index 00000000..19d01de7
--- /dev/null
+++ b/src/request.js
@@ -0,0 +1,21 @@
+const DEFAULT_ENDPOINT = 'https://kdt-frontend.programmers.co.kr'
+
+export const request = async(url='', options) => {
+ try{
+ const res = await fetch(`${DEFAULT_ENDPOINT}${url}`, {
+ ...options,
+ headers: {
+ 'x-username': 'eugene',
+ 'Content-Type': 'application/json'
+ }
+ })
+ if(res.ok){
+ const json = await res.json();
+ return json
+ }
+ throw new Error('API호출 오류');
+ }
+ catch(e){
+ alert(e.message);
+ }
+}
\ No newline at end of file
diff --git a/src/router.js b/src/router.js
new file mode 100644
index 00000000..24eb92e6
--- /dev/null
+++ b/src/router.js
@@ -0,0 +1,17 @@
+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 routerNav = (nextUrl) => {
+ window.dispatchEvent(new CustomEvent(ROUTE_CHANGE_EVENT_NAME, {
+ detail: {
+ nextUrl
+ }
+ }))
+}
\ No newline at end of file
diff --git a/style/DocumentDelete.css b/style/DocumentDelete.css
new file mode 100644
index 00000000..1c6b4391
--- /dev/null
+++ b/style/DocumentDelete.css
@@ -0,0 +1,8 @@
+.delete-btn{
+ background-color: transparent;
+ border:none;
+ color: red;
+ font-family: 'Pretandard-Regular', sans-serif;
+ font-size: 15px;
+ order:1;
+}
\ No newline at end of file
diff --git a/style/DocumentEdit.css b/style/DocumentEdit.css
new file mode 100644
index 00000000..b01d8fb4
--- /dev/null
+++ b/style/DocumentEdit.css
@@ -0,0 +1,66 @@
+.EditorPage{
+ font-family: 'Pretandard-Regular', sans-serif;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ padding-left: 30px;
+ gap: 20px;
+}
+
+.titleInput{
+ border:none;
+ font-size: 27px;
+ font-family: 'Pretandard-Regular', sans-serif;
+ font-weight: 700;
+ color: #37362F;
+ width:600px;
+}
+
+.titleInput:focus{
+ outline: none;
+ border: none;
+}
+
+.contentInput{
+ width:600px;
+ height:400px;
+ border:none;
+ padding:8px;
+ font-family: 'Pretandard-Regular', sans-serif;
+ font-size: 15px;
+ font-weight: 500;
+ padding-top: 20px;
+ resize: none;
+}
+.contentInput:focus{
+ outline: none;
+ border: none;
+}
+.nothing{
+ padding: 10px;
+ padding-left: 15px;
+}
+
+.editor{
+ order: 0;
+}
+.childLink{
+ order:2;
+ display: flex;
+ width: 100%;
+ justify-content: flex-start;
+ flex-direction: column;
+ gap: 5px;
+}
+
+.linkDoc{
+ font-family: 'Pretandard-Regular', sans-serif;
+ font-size: 15px;
+ cursor: pointer;
+ list-style: none;
+}
+.linkDoc::before{
+ content:'🔗 '
+}
\ No newline at end of file
diff --git a/style/DocumentList.css b/style/DocumentList.css
new file mode 100644
index 00000000..7f0d8677
--- /dev/null
+++ b/style/DocumentList.css
@@ -0,0 +1,70 @@
+.documentPage {
+ background-color: #F7F7F5;
+ min-width: 300px;
+ width: 300px;
+ box-sizing: border-box;
+ height: 100%;
+ border-right: 1px solid #e7e7e7;
+ font-family: 'Pretandard-Regular', sans-serif;
+ font-size: 15px;
+ font-weight: 600;
+ color: #72716D;
+ padding: 10px;
+ padding-top:50px;
+ position: relative;
+ order: 0;
+ overflow-y: scroll;
+ height:100vh;
+}
+
+.doc::before{
+ content: "> "
+}
+.open::before{
+ content: "⌵ ";
+}
+
+.doc{
+ list-style: none;
+ cursor:pointer;
+ padding: 12px 0px;
+ position:relative;
+ width:100%;
+}
+
+
+[class^="doc-"]{
+ display:flex;
+ width: 100%;
+ flex-direction: column;
+}
+[class^="root-"]{
+ display:flex;
+ position:relative;
+ width:100%;
+}
+.createDoc{
+ position:absolute;
+ right: 0px;
+ width: 10px;
+ height: 20px;
+ text-align: center;
+ z-index:1;
+ background-color: transparent;
+ border: none;
+ color:#72716D;
+ cursor:pointer;
+}
+.rootCreate{
+ background-color: transparent;
+ border: none;
+ color:#72716D;
+ font-size: 20px;
+ cursor: pointer;
+}
+
+li{
+ :hover{
+ color: black;
+ }
+}
\ No newline at end of file
diff --git a/style/DocumentModal.css b/style/DocumentModal.css
new file mode 100644
index 00000000..0659120e
--- /dev/null
+++ b/style/DocumentModal.css
@@ -0,0 +1,41 @@
+.modal {
+ position: absolute;
+ z-index: 2;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(169, 169, 169, 0.1);
+ display: none;
+ }
+ .modal-content {
+ background-color: #fefefe;
+ margin: 15% auto;
+ padding: 20px;
+ border-radius: 10px;
+ width: 80%;
+ height: 10%;
+ display:flex;
+ flex-direction: column;
+ padding-top:80px;
+ align-items: center;
+ gap:30px;
+ position:relative;
+ }
+ .modalText{
+ width: 500px;
+ border:none;
+ font-family: 'Pretandard-Regular', sans-serif;
+ font-size: 25px;
+ font-weight: 500;
+ }
+ .closeBtn{
+ width: 80px;
+ background-color: transparent;
+ border: none;
+ position :absolute;
+ right: 2px;
+ top : 30px;
+ cursor: pointer;
+ }
diff --git a/style/global.css b/style/global.css
new file mode 100644
index 00000000..1da72d37
--- /dev/null
+++ b/style/global.css
@@ -0,0 +1,23 @@
+
+@font-face {
+ font-family: 'Pretendard-Regular';
+ src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff');
+ font-style: normal;
+}
+
+body {
+ font-family: 'Pretandard-Regular', sans-serif;
+ font-size: 12px;
+ font-weight: 400;
+ position: relative;
+ background-color: white;
+ width:100%;
+ height: 100vh;
+ box-sizing:border-box;
+ margin:0px;
+}
+
+.mainApp {
+ display: flex;
+ flex-direction: row;
+}