diff --git a/index.css b/index.css new file mode 100644 index 00000000..01c6d9a9 --- /dev/null +++ b/index.css @@ -0,0 +1,58 @@ +body { + margin: 0; +} +#app { + width: 100%; + display: flex; +} +.sidebar { + width: 25%; + min-width: 250px; + background-color: rgb(247, 247, 245); + padding: 5px; +} +.sidebar_header { + padding: 8px; + border-bottom: 1px solid black; +} +.sidebar_newDocument { + cursor: pointer; + box-sizing: border-box; + padding: 3px; +} +.sidebar_newDocument:hover { + background-color: rgb(232, 232, 230); +} + +.document_list > ul { + padding-left: 8px; +} +.document_li { + list-style: none; + cursor: pointer; +} + +section { + width: 100%; + padding: 8px; +} +.editor { + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; +} +.title { + flex-grow: 1; + width: 100%; + height: 50px; + margin-bottom: 10px; + padding: 5px 20px; + outline: none; +} +.content { + width: 100%; + height: 100%; + padding: 20px; + outline: none; +} diff --git a/index.html b/index.html new file mode 100644 index 00000000..4d355da4 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + 노션 클로닝 + + + +
+ + + diff --git a/src/api.js b/src/api.js new file mode 100644 index 00000000..88e16d8a --- /dev/null +++ b/src/api.js @@ -0,0 +1,19 @@ +export 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: { + 'x-username': 'whj', + 'Content-Type': 'application/json', + }, + }); + if (res.ok) { + return await res.json(); + } + throw new Error('API처리중 문제 발생!!'); + } catch (e) { + console.log(e.message); + } +}; diff --git a/src/components/App.js b/src/components/App.js new file mode 100644 index 00000000..b242d940 --- /dev/null +++ b/src/components/App.js @@ -0,0 +1,71 @@ +import DocumentListPage from './Sidebar/DocumentListPage.js'; +import EditPage from './Editor/EditPage.js'; +import ListHeader from './Sidebar/ListHeader.js'; +import { request } from '../api.js'; + +export default function App({ $target }) { + const $sidebar = document.createElement('aside'); + $sidebar.className = 'sidebar'; + const $editor = document.createElement('section'); + + new ListHeader({ + $target: $sidebar, + onNewDocument: () => { + console.log('새로운 문서'); + const nextState = { + documentId: 'new', + parent: null, + }; + editPage.setState(nextState); + }, + }); + + const documentListPage = new DocumentListPage({ + $target: $sidebar, + onSelectDocument: (documentId) => { + const nextState = { documentId }; + console.log(nextState); + editPage.setState(nextState); + }, + onCreateDocument: (documentId) => { + const parent = documentId; + const nextState = { + documentId: 'new', + parent, + }; + editPage.setState(nextState); + }, + onRemoveDocument: async (documentId) => { + console.log('remove'); + await request(`/documents/${documentId}`, { + method: 'DELETE', + }); + documentListPage.setState(); + }, + }); + + const editPage = new EditPage({ + $target: $editor, + initialState: { + documentId: 'new', + document: { + title: '', + content: '', + }, + parent: null, + }, + onChange: () => { + documentListPage.setState(); + }, + }); + + this.route = () => { + $target.append($sidebar, $editor); + const { pathname } = window.location; + if (pathname === '/') { + documentListPage.setState(); + editPage.render(); + } + }; + this.route(); +} diff --git a/src/components/Editor/EditPage.js b/src/components/Editor/EditPage.js new file mode 100644 index 00000000..6ccb864a --- /dev/null +++ b/src/components/Editor/EditPage.js @@ -0,0 +1,72 @@ +import { request } from '../../api.js'; +import Editor from './Editor.js'; + +export default function EditPage({ $target, initialState, onChange }) { + const $page = document.createElement('div'); + + this.state = initialState; + + const fetchDocument = async () => { + const { documentId } = this.state; + if (documentId !== 'new') { + const document = await request(`/documents/${documentId}`); //api GET + this.setState({ + ...this.state, + document, + }); + } + }; + + let timer = null; + const editor = new Editor({ + $target: $page, + initialState: this.state.document, + onEditing: (document) => { + if (timer !== null) { + clearTimeout(timer); + } + timer = setTimeout(async () => { + const isNew = this.state.documentId === 'new'; + if (isNew) { + const createdDocument = await request('/documents', { + method: 'POST', + body: JSON.stringify({ + title: document.title, + parent: this.state.parent, + }), + }); + this.setState({ + documentId: createdDocument.id, + document, + }); + onChange(); + } else { + await request(`/documents/${this.state.documentId}`, { + method: 'PUT', + body: JSON.stringify(document), + }); + } + onChange(); + }, 1000); + }, + }); + + this.setState = async (nextState) => { + if (this.state.documentId !== nextState.documentId) { + this.state = nextState; + if (this.state.documentId === 'new') { + this.render(); + editor.setState({ title: '', content: '' }); + } else { + await fetchDocument(); + } + return; + } + this.state = nextState; + editor.setState(this.state.document || { title: '', content: '' }); + }; + + this.render = () => { + $target.appendChild($page); + }; +} diff --git a/src/components/Editor/Editor.js b/src/components/Editor/Editor.js new file mode 100644 index 00000000..ce50cd8e --- /dev/null +++ b/src/components/Editor/Editor.js @@ -0,0 +1,45 @@ +export default function Editor({ $target, initialState, onEditing }) { + const $editor = document.createElement('form'); + $editor.className = 'editor'; + $target.appendChild($editor); + + const $title = document.createElement('input'); + $title.className = 'title'; + $title.name = 'title'; + $title.placeholder = '제목을 작성하세요...'; + + const $content = document.createElement('textarea'); + $content.className = 'content'; + $content.name = 'content'; + $content.placeholder = '내용을 입력하세요...'; + + let isInitialize = false; + + this.state = initialState; + this.setState = (nextState) => { + this.state = nextState; + this.render(); + }; + + $editor.addEventListener('keyup', (e) => { + const name = e.target.getAttribute('name'); + if (this.state[name] !== undefined) { + const nextState = { + ...this.state, + [name]: e.target.value, + }; + this.setState(nextState); + onEditing(this.state); + } + }); + + this.render = () => { + if (!isInitialize) { + $editor.append($title, $content); + isInitialize = true; + } + $title.value = this.state.title; + $content.value = this.state.content; + }; + this.render(); +} diff --git a/src/components/Sidebar/DocumentList.js b/src/components/Sidebar/DocumentList.js new file mode 100644 index 00000000..29826fc3 --- /dev/null +++ b/src/components/Sidebar/DocumentList.js @@ -0,0 +1,73 @@ +export default function DocumentList({ + $target, + initialState, + onSelectDocument, + onCreateDocument, + onRemoveDocument, +}) { + const $list = document.createElement('div'); + $list.className = 'document_list'; + $target.appendChild($list); + + if (Array.isArray(initialState)) { + this.state = initialState; + } + this.setState = (nextState) => { + this.state = nextState; + this.render(); + }; + this.render = () => { + const $ul = document.createElement('ul'); + + this.state.map((documentInfo) => { + const $li = document.createElement('li'); + $li.className = 'document_li'; + $li.setAttribute('data-id', documentInfo.id); + + const $title = document.createElement('span'); + $title.textContent = documentInfo.title; + $title.className = 'title'; + $li.appendChild($title); + + const $createBtn = document.createElement('button'); + $createBtn.textContent = '+'; + $createBtn.className = 'createBtn'; + $li.appendChild($createBtn); + + const $removeBtn = document.createElement('button'); + $removeBtn.textContent = '-'; + $removeBtn.className = 'removeBtn'; + $li.appendChild($removeBtn); + + if (documentInfo.documents.length > 0) { + new DocumentList({ + $target: $li, + initialState: documentInfo.documents, + }); + } + $ul.appendChild($li); + }); + $list.replaceChildren($ul); + + $ul.addEventListener('click', (e) => { + const $li = e.target.closest('li'); + if ($li !== null) { + const documentId = $li.dataset.id; + + // document 선택 + if (e.target.className === 'title') { + onSelectDocument(documentId); + } + //하위 document 생성 + if (e.target.className === 'createBtn') { + onCreateDocument(documentId); + } + // document 삭제 + else if (e.target.className === 'removeBtn') { + onRemoveDocument(documentId); + } + } + }); + }; + this.render(); +} diff --git a/src/components/Sidebar/DocumentListPage.js b/src/components/Sidebar/DocumentListPage.js new file mode 100644 index 00000000..9b499d21 --- /dev/null +++ b/src/components/Sidebar/DocumentListPage.js @@ -0,0 +1,24 @@ +import { request } from '../../api.js'; +import DocumentList from './DocumentList.js'; +export default function DocumentListPage({ + $target, + onSelectDocument, + onCreateDocument, + onRemoveDocument, +}) { + const $page = document.createElement('div'); + $target.appendChild($page); + + const documentList = new DocumentList({ + $target: $page, + initialState: [], + onSelectDocument, + onCreateDocument, + onRemoveDocument, + }); + + this.setState = async () => { + const documents = await request('/documents'); + documentList.setState(documents); + }; +} diff --git a/src/components/Sidebar/ListHeader.js b/src/components/Sidebar/ListHeader.js new file mode 100644 index 00000000..5d3f095a --- /dev/null +++ b/src/components/Sidebar/ListHeader.js @@ -0,0 +1,21 @@ +export default function ListHeader({ $target, onNewDocument }) { + const $header = document.createElement('div'); + $header.className = 'sidebar_header'; + + $target.appendChild($header); + this.render = () => { + const $userInfo = document.createElement('h3'); + $userInfo.textContent = 'Notion 과제중...'; + + const $newDocument = document.createElement('div'); + $newDocument.className = 'sidebar_newDocument'; + $newDocument.textContent = '새 페이지'; + + $header.append($userInfo, $newDocument); + + $newDocument.addEventListener('click', (e) => { + onNewDocument(); + }); + }; + this.render(); +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..f1e09721 --- /dev/null +++ b/src/main.js @@ -0,0 +1,5 @@ +import App from './components/App.js'; + +const $target = document.querySelector('#app'); + +new App({ $target });