diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..ffcfdbf7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+# dependencies
+/node_modules
+/package-lock.json
+/.pnp
+.pnp.js
+/url.js
+# intellij configs
+.idea/
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+
diff --git a/README.md b/README.md
index 82b85e81..961ff3e9 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,35 @@
## 필수 프로젝트
+글 단위를 Document라고 합니다. Document는 Document 여러개를 포함할 수 있습니다.
+- [x] 화면 좌측에 Root Documents를 불러오는 API를 통해 root Documents를 렌더링합니다.
+- [x] Root Documents를 클릭하면 오른쪽 편집기 영역에 해당 Docuement의 Content를 렌더링 합니다.
+- [x] 해당 Root Document에 하위 Document가 있는 경우 , 해당 Document 아래에 트리 형태로 렌더링 합니다.
+- [x] Document Tree에서 각 Document 우측에는 + 버튼이 있습니다. 해당 버튼을 클릭하면 , 클릭한 Document의 하위 Document로 새 Document를 생성하고 편집화면으로 넘깁니다.
+- [x] 편집기에는 기본적으로 저장 버튼이 없습니다. Document Save API를 이용해 지속적으로 서버에 저장되도록 합니다.
+- [x] History API를 이용해 SPA형태로 만듭니다.
+- [x] 루트 URL 접속 시엔 별다른 편집기 선택이 안된 상태입니다.
+- [x] /documents/{documentId} 로 접속시 , 해당 Document content를 불러와 편집기에 로딩합니다.
+- [x] Header 버튼 클릭하면 정상적으로 새로운 Document 생성되는데 초기값 null 들어가는 현상
+- [x] '제목없음' ,'내용없음' 초기에 grey color , input 들어오면 색 변경
+- [x] putMethod 404 Error
+- [x] Editor content 부분 textarea 값 변경 안되는 현상
+- [x] Editor content 부분 입력할때 textarea 2 개행 이상부터 안보이는 현상
+- [x] 상위 Document 삭제하면 하위 Document 같이 삭제가 되야 하는데 상위 Document 밑으로 이동 되는 현상
+- [x] 상위 Document 삭제하면 최하위 Document 삭제 안되는 현상
+- [x] 예외처리 작업, '/documents'-> documentsUrl 변경
+- [x] Editor content 부분 textarea 렌더링 했을때 2 개행 이상부터 잘려서 안나오는 현상
+- [x] Editor content 일정 개행 이상 넘어가면 title 안보이는 현상
+- [x] 버튼 이미지 넣어서 관리 -> 현재 text로 들어가 있음
+- [x] 편집기 하단에는 Documents의 하위 Document 링크를 렌더링하도록 생성
+- [ ] onDelete 리팩토링 필요
+- [ ] 화면 깜빡거리는 현상
+- [ ] 디바운스 적용
+
- 프로젝트 기한
- 프로젝트 수행 기간 : 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 [프로젝트] 노션 클로닝 요구사항** 확인 부탁드립니다.
+ - **Day 17 [프로젝트] 노션 클로닝 요구사항** 확인 부탁드립니다.
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..809daa56
--- /dev/null
+++ b/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+ Title
+
+
+
+
+
+
+
+
+
diff --git a/main.js b/main.js
new file mode 100644
index 00000000..6dd485e7
--- /dev/null
+++ b/main.js
@@ -0,0 +1,5 @@
+import App from './src/App.js';
+
+const app = document.querySelector('.app');
+
+new App({ target: app });
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..7c902b95
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "fedc3-4_project_notion_vanillajs",
+ "version": "1.0.0",
+ "description": "- 프로젝트 기한\r - 프로젝트 수행 기간 : 2022년 11월 8일(화) ~ 2022년 11월 16일(수)\r - 멘티 코드 리뷰 기간 : 2022년 11월 17일(목) ~ 2022년 11월 20일(일)\r - 멘토 코드 리뷰 기간 : 2022년 11월 17일(목) ~ 2022년 11월 22일(화)\r - 코드 리뷰 반영 기간 : 2022년 11월 23일(수) ~ 2022년 11월 25일(금)\r - 내용\r - **Day 17 [프로젝트] 노션 클로닝 요구사항** 확인 부탁드립니다.",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/prgrms-fe-devcourse/FEDC3-4_Project_Notion_VanillaJS.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/prgrms-fe-devcourse/FEDC3-4_Project_Notion_VanillaJS/issues"
+ },
+ "homepage": "https://github.com/prgrms-fe-devcourse/FEDC3-4_Project_Notion_VanillaJS#readme",
+ "devDependencies": {
+ "prettier": "2.7.1"
+ },
+ "prettier": {
+ "singleQuote": true,
+ "semi": true,
+ "useTabs": false,
+ "tabWidth": 2,
+ "trailingComma": "all",
+ "printWidth": 100,
+ "bracketSpacing": true,
+ "arrowParens": "always"
+ }
+}
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 00000000..5ebfd03f
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,34 @@
+import Navbar from './Navbar/Navbar.js';
+import { documentsUrl } from './utils/util.js';
+import { initRouter } from './utils/route.js';
+import Editor from './Editor/Editor.js';
+
+function App({ target }) {
+ const navbarPage = new Navbar({ target });
+ const postEditPage = new Editor({
+ target,
+ initialState: {
+ title: '',
+ content: '',
+ },
+ });
+
+ this.route = async () => {
+ const { pathname } = location;
+
+ if (pathname === '/') {
+ target.innerHTML = '';
+ navbarPage.setState();
+ } else if (pathname.indexOf(`${documentsUrl}/`) === 0) {
+ const [, , postId] = pathname.split('/');
+
+ await navbarPage.setState();
+ await postEditPage.setState({ postId });
+ }
+ };
+
+ this.route();
+ initRouter(() => this.route());
+}
+
+export default App;
diff --git a/src/Editor/Editor.js b/src/Editor/Editor.js
new file mode 100644
index 00000000..1ef50c99
--- /dev/null
+++ b/src/Editor/Editor.js
@@ -0,0 +1,75 @@
+import { createElement } from '../utils/dom.js';
+import { isNew, isObject } from '../utils/errorHandler.js';
+import { request } from '../utils/api.js';
+import { putContentMethod, putTitleMethod } from '../utils/optionsMethod.js';
+import EditorTitle from './EditorTitle.js';
+import EditorContent from './EditorContent.js';
+import { documentsUrl } from '../utils/util.js';
+import { route } from '../utils/route.js';
+import EditorSubContent from './EditorSubContent.js';
+import { ERROR_MESSAGE } from '../utils/constant.js';
+
+function Editor({ target, initialState }) {
+ isNew(new.target);
+ const page = createElement('section');
+ page.className = 'content';
+
+ const editor = createElement('div');
+ editor.className = 'editor';
+
+ this.state = initialState;
+
+ const postTitle = new EditorTitle({
+ div: editor,
+ initialState: initialState,
+
+ onChangeTitle: async ({ id, title }) => {
+ setTimeout(await putTitleMethod(id, title), 2000);
+ },
+ });
+
+ const postContent = new EditorContent({
+ div: editor,
+ initialState: initialState,
+
+ onChangeContent: async ({ id, content }) => {
+ setTimeout(await putContentMethod(id, content), 2000);
+ },
+ });
+
+ const postSub = new EditorSubContent({
+ div: editor,
+ initialState: initialState,
+ onClickSubDocument: async (id) => {
+ await changeUrl(id);
+ },
+ });
+
+ this.setState = async (nextState) => {
+ const post = await request(`${documentsUrl}/${nextState.postId}`);
+ isObject(post);
+ postTitle.setState(post);
+ postContent.setState(post);
+ postSub.setState(post);
+ render();
+ };
+
+ const render = () => {
+ page.appendChild(editor);
+ target.appendChild(page);
+ };
+
+ const changeUrl = async (id) => {
+ try {
+ const post = await request(`${documentsUrl}/${id}`);
+ route(`${documentsUrl}/${post.id}`);
+ } catch (e) {
+ alert(ERROR_MESSAGE.EMPTY);
+ route('/');
+ }
+ };
+
+ render();
+}
+
+export default Editor;
diff --git a/src/Editor/EditorContent.js b/src/Editor/EditorContent.js
new file mode 100644
index 00000000..1b5a36e4
--- /dev/null
+++ b/src/Editor/EditorContent.js
@@ -0,0 +1,47 @@
+import { createElement } from '../utils/dom.js';
+import { isNew, isString } from '../utils/errorHandler.js';
+
+function EditorContent({ div, initialState, onChangeContent }) {
+ isNew(new.target);
+ const editorContent = createElement('div');
+ editorContent.className = 'editor-content';
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ const { content } = this.state;
+
+ render();
+ editorContent.querySelector('.content').value = content && content;
+ };
+
+ const render = () => {
+ const { content } = this.state;
+ editorContent.innerHTML = ``;
+
+ div.appendChild(editorContent);
+ };
+
+ const onKeyUpContent = () => {
+ editorContent.addEventListener('keyup', (e) => {
+ const { name } = e.target;
+ const contentValue = e.target.value;
+ isString(contentValue);
+ const nextState = {
+ ...this.state,
+ [name]: contentValue,
+ };
+
+ onChangeContent(nextState);
+ });
+ };
+
+ render();
+ onKeyUpContent();
+}
+
+export default EditorContent;
diff --git a/src/Editor/EditorSubContent.js b/src/Editor/EditorSubContent.js
new file mode 100644
index 00000000..f37921ab
--- /dev/null
+++ b/src/Editor/EditorSubContent.js
@@ -0,0 +1,49 @@
+import { createElement, targetClosest } from '../utils/dom.js';
+
+function EditorSubContent({ div, initialState, onClickSubDocument }) {
+ const editorDocuments = createElement('div');
+ editorDocuments.className = 'editor-sub-document';
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ render();
+ };
+
+ const render = () => {
+ div.appendChild(editorDocuments);
+
+ const { documents } = this.state;
+ editorDocuments.innerHTML = paintSubDocuments(documents);
+ };
+
+ const paintSubDocuments = (documents) => {
+ if (documents === undefined) {
+ return false;
+ } else {
+ return documents
+ .map(
+ ({ id, title }) => `
+
+
+
+ ${title ? title : '제목없음'}
+ `,
+ )
+ .join('');
+ }
+ };
+
+ const onClickEditorSubDocument = () => {
+ editorDocuments.addEventListener('click', (e) => {
+ const subPostId = targetClosest(e, 'div').id;
+ onClickSubDocument(subPostId);
+ });
+ };
+
+ render();
+ onClickEditorSubDocument();
+}
+
+export default EditorSubContent;
diff --git a/src/Editor/EditorTitle.js b/src/Editor/EditorTitle.js
new file mode 100644
index 00000000..44f58ef2
--- /dev/null
+++ b/src/Editor/EditorTitle.js
@@ -0,0 +1,44 @@
+import { createElement } from '../utils/dom.js';
+import { isNew, isString } from '../utils/errorHandler.js';
+
+function EditorTitle({ div, initialState, onChangeTitle }) {
+ isNew(new.target);
+ const editorTitle = createElement('div');
+ editorTitle.className = 'editor-title';
+ this.state = initialState;
+
+ div.appendChild(editorTitle);
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ const { title } = this.state;
+ render();
+ editorTitle.querySelector('.title').value = title && title;
+ };
+
+ const render = () => {
+ const { title } = this.state;
+ editorTitle.innerHTML = ` `;
+ div.appendChild(editorTitle);
+ };
+
+ const onKeyupEditorTitle = () => {
+ editorTitle.addEventListener('keyup', (e) => {
+ const { name } = e.target;
+ const titleValue = e.target.value;
+
+ isString(titleValue);
+
+ const nextState = {
+ ...this.state,
+ [name]: titleValue,
+ };
+
+ onChangeTitle(nextState);
+ });
+ };
+ render();
+ onKeyupEditorTitle();
+}
+
+export default EditorTitle;
diff --git a/src/Navbar/Navbar.js b/src/Navbar/Navbar.js
new file mode 100644
index 00000000..164ee965
--- /dev/null
+++ b/src/Navbar/Navbar.js
@@ -0,0 +1,87 @@
+import { isNew } from '../utils/errorHandler.js';
+import { createElement } from '../utils/dom.js';
+import { request } from '../utils/api.js';
+import { documentsUrl } from '../utils/util.js';
+import { addMethod, deleteMethod } from '../utils/optionsMethod.js';
+import { route } from '../utils/route.js';
+import NavbarDocumentList from './NavbarDocumentList.js';
+import NavbarHeader from './NavbarHeader.js';
+
+function Navbar({ target }) {
+ isNew(new.target);
+ const nav = createElement('nav');
+ nav.className = 'sidebar';
+
+ new NavbarHeader({
+ target: nav,
+ initialState: {
+ text: 'Notion',
+ },
+ });
+
+ const documentList = new NavbarDocumentList({
+ target: nav,
+ initialState: [],
+ onDelete: async (id) => {
+ const findCurrentId = () => {
+ const rootDocument = [...documentList.state];
+
+ while (rootDocument.length > 0) {
+ const cur = rootDocument.shift();
+
+ if (parseInt(cur.id) === parseInt(id)) {
+ console.log(cur);
+ return cur;
+ }
+
+ cur.documents.forEach((el) => {
+ rootDocument.push(el);
+ });
+ }
+ };
+ // 현재 하위 document 가 3개 이상이면 하위 document 값이 삭제가 안되고 있음
+ // 하위 document 2개씩만 삭제되는 현상
+
+ const handlerDelete = async () => {
+ const currentDocument = findCurrentId();
+
+ let que = [currentDocument];
+
+ const queId = [currentDocument.id];
+
+ while (que.length > 0) {
+ const current = que.shift();
+ current.documents.forEach((el) => {
+ que.push(el);
+ queId.push(el.id);
+ });
+
+ for (let i of queId) {
+ await deleteMethod(i);
+ que = [];
+ }
+ }
+ };
+
+ await handlerDelete();
+ route('/');
+ },
+
+ onAdd: async (id) => {
+ const newPost = await addMethod(id);
+ route(`${documentsUrl}/${newPost.id}`);
+ },
+ });
+
+ this.setState = async () => {
+ const posts = await request(`${documentsUrl}`);
+ documentList.setState(posts);
+ this.render();
+ };
+
+ this.render = async () => {
+ target.appendChild(nav);
+ };
+}
+
+export default Navbar;
diff --git a/src/Navbar/NavbarDocumentList.js b/src/Navbar/NavbarDocumentList.js
new file mode 100644
index 00000000..58133906
--- /dev/null
+++ b/src/Navbar/NavbarDocumentList.js
@@ -0,0 +1,72 @@
+import { createElement, targetClosest, targetContains } from '../utils/dom.js';
+import { isNew } from '../utils/errorHandler.js';
+import { route } from '../utils/route.js';
+import { addBtnElement, deleteBtnElement, documentsUrl } from '../utils/util.js';
+
+function NavbarDocumentList({ target, initialState, onDelete, onAdd }) {
+ isNew(new.target);
+ const postList = createElement('div');
+
+ target.appendChild(postList);
+
+ this.state = initialState;
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ render();
+ };
+
+ const render = () => {
+ postList.innerHTML = paintDocument(this.state);
+ onClickDocument();
+ };
+
+ const paintDocument = (docuements) => {
+ return `
+
+
+ ${docuements
+ .map(
+ (post) => `
+
+
+
+
+
+ ${post.title ? post.title : '제목 없음'}
+
+
+
+
+
+
+
+
+ ${post.documents.length ? paintDocument(post.documents) : ''}
+
+ `,
+ )
+ .join('')}
+
+ `;
+ };
+
+ const onClickDocument = () => {
+ const ul = postList.querySelector('.document-list');
+
+ ul.addEventListener('click', (e) => {
+ const postId = targetClosest(e, 'li').id;
+
+ if (targetContains(e, deleteBtnElement)) {
+ onDelete(postId);
+ } else if (targetContains(e, addBtnElement)) {
+ onAdd(postId);
+ } else {
+ route(`${documentsUrl}/${postId}`);
+ }
+ });
+ };
+ render();
+}
+
+export default NavbarDocumentList;
diff --git a/src/Navbar/NavbarHeader.js b/src/Navbar/NavbarHeader.js
new file mode 100644
index 00000000..322f0024
--- /dev/null
+++ b/src/Navbar/NavbarHeader.js
@@ -0,0 +1,38 @@
+import { createElement, targetClosest } from '../utils/dom.js';
+import { isNew } from '../utils/errorHandler.js';
+import { route } from '../utils/route.js';
+import { documentsUrl } from '../utils/util.js';
+import { addHeaderMethod } from '../utils/optionsMethod.js';
+
+function NavbarHeader({ target, initialState }) {
+ isNew(new.target);
+
+ const header = createElement('div');
+ header.className = 'header';
+ this.state = initialState;
+
+ const render = () => {
+ header.innerHTML = `
+ ${this.state.text}
+
+
+ `;
+ target.appendChild(header);
+ };
+
+ const onClickHeader = () => {
+ header.addEventListener('click', async (e) => {
+ const homeBtn = targetClosest(e, '.home-btn');
+ if (homeBtn) {
+ route('/');
+ } else {
+ const createPost = await addHeaderMethod(documentsUrl);
+ route(`${documentsUrl}/${createPost.id}`);
+ }
+ });
+ };
+ render();
+ onClickHeader();
+}
+
+export default NavbarHeader;
diff --git a/src/style/index.css b/src/style/index.css
new file mode 100644
index 00000000..480be69d
--- /dev/null
+++ b/src/style/index.css
@@ -0,0 +1,5 @@
+@import 'ui/index.css';
+html {
+ visibility: visible;
+ opacity: 1;
+}
diff --git a/src/style/ui/index.css b/src/style/ui/index.css
new file mode 100644
index 00000000..d8354840
--- /dev/null
+++ b/src/style/ui/index.css
@@ -0,0 +1,3 @@
+@import 'modules/navbar.css';
+@import 'modules/editor.css';
+@import 'modules/reset.css';
diff --git a/src/style/ui/modules/editor.css b/src/style/ui/modules/editor.css
new file mode 100644
index 00000000..ae730cd4
--- /dev/null
+++ b/src/style/ui/modules/editor.css
@@ -0,0 +1,45 @@
+.app {
+ display: flex;
+ height: 100vh;
+}
+
+.content {
+ width: 100%;
+ height: 70%;
+ border: none;
+ outline: none;
+ overflow: auto;
+ font-size: 20px;
+ display: flex;
+ justify-content: space-around;
+ margin-top: 5%;
+ /*-ms-overflow-style: none;*/
+}
+
+.content ::-webkit-scrollbar {
+ display: none;
+ height: 100em;
+}
+
+.editor {
+ width: 60%;
+}
+
+.title {
+ width: 100%;
+ display: block;
+ border: none;
+ outline: none;
+ font-size: 38px;
+}
+
+.add {
+ width: 25px;
+}
+
+.editor-content {
+ height: 40vh;
+}
+
+.editor-sub-document {
+}
diff --git a/src/style/ui/modules/navbar.css b/src/style/ui/modules/navbar.css
new file mode 100644
index 00000000..86eb5868
--- /dev/null
+++ b/src/style/ui/modules/navbar.css
@@ -0,0 +1,82 @@
+.header {
+ margin-bottom: 30px;
+}
+
+.document-list {
+ padding-left: 10px;
+}
+
+.list-style-type {
+ list-style-type: none;
+}
+
+.sidebar {
+ width: 20%;
+ font-size: 15px;
+ line-height: 1rem;
+ background-color: rgb(245, 244, 241);
+ padding: 1em;
+}
+
+.row-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 5px;
+ min-height: 27px;
+ color: #cccbc7;
+ cursor: pointer;
+ padding-left: 10px;
+}
+
+.row-item:hover {
+ background: rgba(55, 53, 47, 0.08);
+ color: black;
+}
+
+.item-text {
+ display: flex;
+ width: 75%;
+ align-items: center;
+ margin-left: 10px;
+}
+
+.item-button {
+ display: flex;
+ flex-direction: row-reverse;
+ /*align-items: center;*/
+ width: inherit;
+ /*padding-left: 20px;*/
+}
+
+button {
+ border: none;
+ outline: none;
+ padding: 0;
+ background: none;
+ margin-left: 30px;
+ cursor: pointer;
+}
+
+button:hover {
+ border: solid 1px #aca99e;
+ border-radius: 5px;
+ color: black;
+}
+
+.delete-btn {
+ font-size: 18px;
+}
+
+.add-btn {
+ font-size: 18px;
+}
+
+.home-btn {
+ position: absolute;
+ left: 13%;
+}
+.home-plus-btn {
+ position: absolute;
+ left: 3rem;
+}
diff --git a/src/style/ui/modules/reset.css b/src/style/ui/modules/reset.css
new file mode 100644
index 00000000..e44f2d21
--- /dev/null
+++ b/src/style/ui/modules/reset.css
@@ -0,0 +1,136 @@
+/* Reset Browser */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+html,
+body,
+div,
+span,
+applet,
+object,
+iframe,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+blockquote,
+pre,
+a,
+abbr,
+acronym,
+address,
+big,
+cite,
+code,
+del,
+dfn,
+em,
+img,
+ins,
+kbd,
+q,
+s,
+samp,
+small,
+strike,
+strong,
+sub,
+sup,
+tt,
+var,
+b,
+u,
+i,
+center,
+dl,
+dt,
+dd,
+ol,
+fieldset,
+form,
+label,
+legend,
+table,
+caption,
+tbody,
+tfoot,
+thead,
+tr,
+th,
+td,
+article,
+aside,
+canvas,
+details,
+embed,
+figure,
+figcaption,
+footer,
+header,
+hgroup,
+menu,
+nav,
+output,
+ruby,
+section,
+summary,
+time,
+mark,
+audio,
+video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+
+/* HTML5 display-role reset for browsers */
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+menu,
+nav,
+section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol,
+ul {
+ list-style: none;
+}
+blockquote,
+q {
+ quotes: none;
+}
+blockquote:before,
+blockquote:after,
+q:before,
+q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+input:focus {
+ outline: none;
+}
+a {
+ color: inherit;
+ text-decoration: none;
+}
diff --git a/src/utils/api.js b/src/utils/api.js
new file mode 100644
index 00000000..754b0dba
--- /dev/null
+++ b/src/utils/api.js
@@ -0,0 +1,19 @@
+import { API_END_POINT } from '../../url.js';
+
+export const request = async (url, option = {}) => {
+ try {
+ const res = await fetch(`${API_END_POINT}${url}`, {
+ ...option,
+ headers: {
+ 'x-username': 'hwa',
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (res.ok) {
+ return await res.json();
+ }
+ } catch (e) {
+ alert(e.message);
+ }
+};
diff --git a/src/utils/constant.js b/src/utils/constant.js
new file mode 100644
index 00000000..865b3b78
--- /dev/null
+++ b/src/utils/constant.js
@@ -0,0 +1,6 @@
+export const ERROR_MESSAGE = {
+ NEW: 'NEW 연산자가 없습니다.',
+ OBJECT: '객체 타입이 아닙니다.',
+ STRING: '문자열 타입이 아닙니다.',
+ EMPTY: '해당 문서를 찾을 수 없습니다',
+};
diff --git a/src/utils/dom.js b/src/utils/dom.js
new file mode 100644
index 00000000..337f5ea9
--- /dev/null
+++ b/src/utils/dom.js
@@ -0,0 +1,3 @@
+export const createElement = (e) => document.createElement(e);
+export const targetClosest = (e, str) => e.target.closest(str);
+export const targetContains = (e, str) => e.target.classList.contains(str);
diff --git a/src/utils/errorHandler.js b/src/utils/errorHandler.js
new file mode 100644
index 00000000..23fc757b
--- /dev/null
+++ b/src/utils/errorHandler.js
@@ -0,0 +1,19 @@
+import { ERROR_MESSAGE } from './constant.js';
+
+export const isNew = (target) => {
+ if (!target) {
+ throw new Error(ERROR_MESSAGE.NEW);
+ }
+};
+
+export const isString = (title) => {
+ if (typeof title !== 'string') {
+ throw new Error(ERROR_MESSAGE.STRING);
+ }
+};
+
+export const isObject = (post) => {
+ if (!post instanceof Object) {
+ throw new Error(ERROR_MESSAGE.OBJECT);
+ }
+};
diff --git a/src/utils/optionsMethod.js b/src/utils/optionsMethod.js
new file mode 100644
index 00000000..adc59edb
--- /dev/null
+++ b/src/utils/optionsMethod.js
@@ -0,0 +1,46 @@
+import { request } from './api.js';
+import { documentsUrl } from './util.js';
+
+export const putTitleMethod = (id, title) => {
+ return request(`${documentsUrl}/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify({
+ title: title,
+ }),
+ });
+};
+
+export const putContentMethod = (id, content) => {
+ return request(`${documentsUrl}/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify({
+ content: content,
+ }),
+ });
+};
+
+export const addMethod = (id) => {
+ return request(`${documentsUrl}`, {
+ method: 'POST',
+ body: JSON.stringify({
+ title: '',
+ parent: id,
+ }),
+ });
+};
+
+export const addHeaderMethod = (documentsUrl) => {
+ return request(`${documentsUrl}`, {
+ method: 'POST',
+ boyd: JSON.stringify({
+ title: '',
+ parent: null,
+ }),
+ });
+};
+
+export const deleteMethod = (id) => {
+ return request(`${documentsUrl}/${id}`, {
+ method: 'DELETE',
+ });
+};
diff --git a/src/utils/route.js b/src/utils/route.js
new file mode 100644
index 00000000..2a92ccb7
--- /dev/null
+++ b/src/utils/route.js
@@ -0,0 +1,24 @@
+import { ROUTE_CHANGE_NAME } from './util.js';
+
+export const initRouter = (onRoute) => {
+ window.addEventListener(ROUTE_CHANGE_NAME, (e) => {
+ const { nextUrl } = e.detail;
+
+ if (nextUrl) {
+ history.pushState(null, null, nextUrl);
+ onRoute();
+ }
+ });
+};
+
+export const route = (nextUrl) => {
+ if (nextUrl) {
+ window.dispatchEvent(
+ new CustomEvent(ROUTE_CHANGE_NAME, {
+ detail: {
+ nextUrl,
+ },
+ }),
+ );
+ }
+};
diff --git a/src/utils/util.js b/src/utils/util.js
new file mode 100644
index 00000000..0415ff6e
--- /dev/null
+++ b/src/utils/util.js
@@ -0,0 +1,4 @@
+export const documentsUrl = '/documents';
+export const ROUTE_CHANGE_NAME = 'route-change';
+export const deleteBtnElement = 'fa-minus-square';
+export const addBtnElement = 'fa-plus-square';