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 });