-
Notifications
You must be signed in to change notification settings - Fork 28
[Mission4/조예지] - Project_Notion_VanillaJS #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
yezyvibe
wants to merge
24
commits into
prgrms-fe-devcourse:3/#4_joyeji
Choose a base branch
from
yezyvibe:3/#4_joyeji_working
base: 3/#4_joyeji
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
7163b3f
docs: :memo: 프로젝트 정의서 작성(README.md)
yezyvibe 41ec3f5
chore: :package: package.json 설정
yezyvibe d01fb47
feat: :tada: 프로젝트 구조 및 페이지 레이아웃 설정
yezyvibe d5af6e4
feat: :sparkles: 문서 목록 트리 형태로 출력, 새로운 문서 생성 기능 구현
yezyvibe d183a50
feat: :sparkles: 선택된 문서의 정보 출력, 편집기 열기
yezyvibe 87b7365
feat: :sparkles: 문서 수정 기능 추가
yezyvibe d5ac93e
feat: :sparkles: 문서 수정 이벤트 디바운싱 처리
yezyvibe fd0c4c4
feat: :sparkles: 문서 삭제 기능, 데이터 로딩 상태 표시 추가
yezyvibe b0ce8eb
feat: :sparkles: 현재 편집중인 문서의 하위 문서들의 링크 추가
yezyvibe 64ca28a
feat: :sparkles: 문서 내용 마크다운으로 출력
yezyvibe b12dcc9
feat: :lipstick: 문서 트리 아이콘 변경 및 css 수정
yezyvibe 854774c
refactor: :recycle: 불필요한 함수 제거 및 함수 분리, 문서 삭제 시 상태값 변경 로직 수정
yezyvibe baae236
feat: :sparkles: 문서 편집기 미리보기 추가
yezyvibe b6ccbf0
feat: :lipstick: 하위 문서 출력 및 이동 이벤트 추가, css 수정
yezyvibe 43fc1db
refactor: :recycle: 에디터 컴포넌트 내 함수 분리, 클래스명 상수화
yezyvibe 2936ef1
refactor: :recycle: DocumentTree 리팩토링, 클래스명과 사용자 메세지 상수화
yezyvibe ead265d
refactor: :recycle: DocumentNode 리팩토링, 클래스명 상수화
yezyvibe da432f1
docs: :memo: README.md 수정, 컴포넌트 구조 및 느낀점 추가
yezyvibe 22ed6b0
refactor: :recycle: api 통신 로직 폴더 이동, 메소드명 변경
yezyvibe 05f4da4
refactor: :recycle: 불필요한 로그
yezyvibe e0f5f73
refactor: :recycle: 디바운스 변수 this 바인딩 제거, pathname에서 문서id 불러오는 중복 로직 함수화
yezyvibe dee02f7
fix: :bug: 문서 상세 페이지 url 수정 document->documents
yezyvibe a7e087e
fix: :bug: redirects 설정, 페이지 새로고침 404 에러 해결
yezyvibe f26a029
refactor: :recycle: package.json 제거 및 debounce util 함수로 분리
yezyvibe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,87 @@ | ||
| # 📌 4주차 프로젝트[Project1] | ||
| # 📝YENOTION | ||
|
|
||
| ## 필수 프로젝트 | ||
|
|
||
| - 프로젝트 기한 | ||
| - 프로젝트 수행 기간 : 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 [프로젝트] 노션 클로닝 요구사항** 확인 부탁드립니다. | ||
|
|
||
| ## 🔸 프로젝트 소개 및 수행 기간 | ||
|
|
||
| * API를 활용하여 문서 자동 저장 편집기를 구현합니다. 필수 기능을 구현 후 추가 기능을 자유롭게 구현합니다. | ||
|
|
||
| > - 프로젝트 수행 기간 : 2022년 11월 8일(화) ~ 2022년 11월 16일(수) | ||
| > - 멘티 코드 리뷰 기간 : 2022년 11월 17일(목) ~ 2022년 11월 20일(일) | ||
| > - 멘토 코드 리뷰 기간 : 2022년 11월 17일(목) ~ 2022년 11월 22일(화) | ||
| > - 코드 리뷰 반영 기간 : 2022년 11월 23일(수) ~ 2022년 11월 25일(금) | ||
|
|
||
| ## 🔸 구조 설계 | ||
|
|
||
| ### 와이어프레임 | ||
|
|
||
| <img src="README.assets/yenotion_wireframe (1).png" alt="yenotion_wireframe (1)" style="zoom:50%;" /> | ||
|
|
||
|
|
||
|
|
||
| ### 컴포넌트 구조 | ||
|
|
||
| <img src="README.assets/yenotion.png" alt="yenotion" style="zoom: 80%;" /> | ||
|
|
||
| ## 🔸 기능 명세 | ||
|
|
||
| ### 기본 기능 | ||
|
|
||
| - [x] 메인 페이지 왼쪽에 전체 문서 트리를 출력합니다. | ||
| - 루트 문서를 클릭하면 오른쪽 편집기 영역에 문서 내용을 렌더링합니다. | ||
| - 문서 이름이 특정 길이를 넘으면 특정 길이까지 자르고 ... 생략으로 표시한다. | ||
|
|
||
| - [x] 새로운 문서를 생성할 수 있습니다. | ||
| - 각 문서의 우측에 + 버튼이 있고, 해당 버튼을 클릭하면 하위에 새로운 문서 생성되고 편집 화면으로 넘어갑니다. | ||
| - 루트 문서 또는 특정 문서 하위에 새로운 문서를 추가할 수 있습니다. | ||
|
|
||
| - [x] 문서를 수정하고 저장할 수 있습니다. | ||
| - 문서 저장 버튼을 따로 만들지 않습니다. | ||
| - document save api를 이용, 서버에 지속적인 요청을 통해 저장되도록 설정합니다. | ||
| - 문서의 내용은 마크다운 문법을 사용하여 나타낼 수 있습니다. | ||
|
|
||
| - 오른편 문서 미리보기에서 실제 페이지의 내용을 확인할 수 있습니다. | ||
|
|
||
| - [x] 문서를 삭제할 수 있습니다. | ||
|
|
||
| - [x] History API를 활용하여 SPA로 제작합니다. | ||
| - 루트 URL: 문서 선택이 아직 안 된 상태를 보여줍니다. | ||
| * 편집기에는 기본 페이지 출력 | ||
| - /documents/{documentId}: 해당 문서의 내용을 불러와 편집기에 출력 | ||
|
|
||
|
|
||
|
|
||
| ### 추가 기능 | ||
|
|
||
| [과제 제안] | ||
|
|
||
| - [x] 기본적으로 편집기는 textarea 기반으로 단순한 텍스트 편집기로 시작하되, 여력이 되면 div와 contentEditable을 조합해서 좀 더 Rich한 에디터를 만들어봅니다. | ||
|
|
||
| - [x] 편집기 최하단에는 현재 편집 중인 문서의 하위 문서들의 링크를 추가. | ||
|
|
||
| - [ ] `페이지 멘션`: 편집기 내에서 @시작해서 다른 문서의 title을 적은 경우, 자동으로 해당 Document의 편집 페이지로 이동하는 링크를 거는 기능을 추가합니다. | ||
|
|
||
| [기타 아이디어 정리] | ||
|
|
||
| * 마크다운 편집기 만들기 | ||
| * 검색 기능 | ||
| * 다크모드 | ||
| * 임베드 | ||
| * 북마크 | ||
| * 하이라이팅 | ||
| * 이미지 임베딩 | ||
| * 공유하기 | ||
| * undo, redo | ||
| * 수정이력 출력 | ||
| * 과거 상태로 돌아가기 | ||
|
|
||
|
|
||
|
|
||
| ## 🔸느낀점 | ||
|
|
||
| * 계획을 거창하게 세우고 시작했지만, 결론적으로 추가 기능을 많이 구현하지 못했다. 일주일 온전히 프로젝트를 수행했으면 진도를 더 많이 나갈 수 있었겠지만,, 그래도 다음 개발 일정 산정은 더 잘해보자! | ||
| * SPA로 개발 시 페이지 새로고침 시 문제가 발생하는 점 -> express 같은 서버를 따로 두는 것이 필요할 것 같다. | ||
| * 이번 프로젝트에서 API 주소를 가리기 위해 API 주소를 담은 파일을 gitignore로 설정하여 git에 올라가지 않도록 처리했지만 -> 이런 경우 어떻게 배포를 할 수 있을지와 같은 고민과, 환경 변수로 정의해서 사용했으면 더 좋았을 거 같다는 생각이 들었다. | ||
| * API 비동기 통신 로직을 따로 utils 함수로 분리해주었는데, 함수를 계속 분리하다 보니 API 요청하고 응답 받아온 후 데이터를 사용하는 부분에서 살짝 버벅였다.. async await 구문을 사용할 때 반환값이 promise이기 때문에 바로 하위 컴포넌트로 넘기지 않고 -> setState로 부모에 저장하고 이후 하위 컴포넌트에 값을 전달했다. -> 처음에 비동기 통신하는 일련의 과정(비동기 요청, 응답 후 상태값 변경 후 리렌더링)이 잘 안돼서 꼼꼼히 확인하면서 작업했다. | ||
| * 바닐라 자바스크립트로 작업하는 건 재밌게 느껴지는데, css에서 class명 관리 및 스타일 속성 재사용, 상태값 관리, 리렌더링 등 신경써야 하는 것들이 많아서 라이브러리, 프레임워크 사용이 훨씬 편하긴 하다. | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /* /index.html 200 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>YENOTION</title> | ||
| <link rel="stylesheet" href="/src/styles/style.css" /> | ||
| <link | ||
| rel="stylesheet" | ||
| href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,200,0,0" | ||
| /> | ||
| </head> | ||
| <body> | ||
| <main id="App"></main> | ||
| <script src="/src/index.js" type="module"></script> | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import Header from "./components/Header.js"; | ||
| import DocumentEditPage from "./pages/DocumentEditPage.js"; | ||
|
|
||
| function App({ $target }) { | ||
| new Header({ $target }); | ||
| new DocumentEditPage({ $target }); | ||
| } | ||
|
|
||
| export default App; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export const API_END_POINT = "https://kdt-frontend.programmers.co.kr"; | ||
|
|
||
| export const HEADERS = { | ||
| "x-username": "yezyvibe", | ||
| "Content-Type": "application/json", | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { request } from "./index.js"; | ||
|
|
||
| export const getRootDocuments = async () => { | ||
| const response = await request("/documents"); | ||
| return response; | ||
| }; | ||
|
|
||
| export const createNewDocument = async (data) => { | ||
| const response = await request("/documents", "POST", data); | ||
| return response; | ||
| }; | ||
|
|
||
| export const getDocumentDetail = async (documentId) => { | ||
| const response = await request(`/documents/${documentId}`); | ||
| return response; | ||
| }; | ||
|
|
||
| export const updateDocument = async (documentId, data) => { | ||
| const response = await request(`/documents/${documentId}`, "PUT", data); | ||
| return response; | ||
| }; | ||
|
|
||
| export const deleteDocument = async (documentId) => { | ||
| const response = await request(`/documents/${documentId}`, "DELETE"); | ||
| return response; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { API_END_POINT, HEADERS } from "./baseUrl.js"; | ||
|
|
||
| export const request = async (url, method = "GET", data, defaultValue = []) => { | ||
| try { | ||
| const response = await fetch(API_END_POINT + url, { | ||
| method, | ||
| headers: HEADERS, | ||
| body: JSON.stringify(data), | ||
| }); | ||
|
|
||
| if (response.ok) { | ||
| const json = await response.json(); | ||
| return json; | ||
| } | ||
|
|
||
| return defaultValue; | ||
| } catch (e) { | ||
| console.log(e.message); | ||
| } | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { navigate } from "../utils/router.js"; | ||
| import EditorPreview from "./EditorPreview.js"; | ||
| import { className } from "../utils/constants.js"; | ||
| import { debounce } from "../utils/debounce.js"; | ||
|
|
||
| function DocumentEditor({ $target, initialState, onChange }) { | ||
| const { editorDocumentTitle, editorDocumentContent } = className; | ||
|
|
||
| const $editor = document.createElement("section"); | ||
| $editor.className = "document-editor"; | ||
| $target.appendChild($editor); | ||
|
|
||
| this.state = initialState; | ||
|
|
||
| this.setState = (nextState) => { | ||
| this.state = nextState; | ||
| editorPreview.setState({ | ||
| data: this.state.data, | ||
| }); | ||
| }; | ||
|
|
||
| const editorPreview = new EditorPreview({ | ||
| $target, | ||
| initialState: { | ||
| data: this.state.data, | ||
| }, | ||
| }); | ||
|
|
||
| const render = () => { | ||
| const { title, content, documents } = this.state.data; | ||
|
|
||
| if (!title) { | ||
| $editor.innerHTML = ` | ||
| <h1>loading..</h1> | ||
| `; | ||
| return; | ||
| } | ||
|
|
||
| $editor.innerHTML = ` | ||
| <input class="${editorDocumentTitle}" value="${ | ||
| title ? title : "제목 없음" | ||
| }"/> | ||
| <article class="eidtor-wrapper"> | ||
| <div class="${editorDocumentContent}" autofocus="true" contenteditable="true">${ | ||
|
yezyvibe marked this conversation as resolved.
|
||
| content ? content : "" | ||
| }</div> | ||
| </article> | ||
| ${ | ||
| documents.length > 0 | ||
| ? `<div class="child-documents-wrapper"> | ||
| <p>📌하위 문서 목록</p> | ||
| ${documents | ||
| .map(({ id, title }) => `<a href="/documents/${id}">${title}</a>`) | ||
| .join("")} | ||
| </div>` | ||
| : "" | ||
| } | ||
| `; | ||
| }; | ||
|
|
||
| render(); | ||
|
|
||
| const updateDocument = () => { | ||
| const $title = document.querySelector(`.${editorDocumentTitle}`); | ||
| const $content = document.querySelector(`.${editorDocumentContent}`); | ||
|
|
||
| onChange($title.value, $content.innerText); | ||
| }; | ||
|
|
||
| $editor.addEventListener("keyup", (e) => { | ||
| const $div = e.target.closest("div"); | ||
|
|
||
| if ($div) { | ||
| debounce(updateDocument, 500); | ||
| } | ||
| }); | ||
|
|
||
| $editor.addEventListener("click", (e) => { | ||
| e.preventDefault(); | ||
|
|
||
| const $a = e.target.closest("a"); | ||
|
|
||
| if ($a) { | ||
| const [, , , , documentId] = $a.href.split("/"); | ||
|
yezyvibe marked this conversation as resolved.
|
||
| navigate(`/documents/${documentId}`); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| export default DocumentEditor; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import { navigate } from "../utils/router.js"; | ||
| import { className } from "../utils/constants.js"; | ||
|
|
||
| function DocumentNode({ $target, initialState, onCreate, onDelete }) { | ||
| const $node = document.createElement("article"); | ||
| $target.appendChild($node); | ||
|
|
||
| const { addButton, deleteButton } = className; | ||
|
|
||
| this.state = initialState; | ||
|
|
||
| this.setState = (nextState) => { | ||
| this.state = nextState; | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.render = () => { | ||
| const { data } = this.state; | ||
|
|
||
| $node.innerHTML = ` | ||
| <ul class="tree-node"> | ||
| ${data | ||
| .map(({ id, title, documents }) => { | ||
| const $li = document.createElement("li"); | ||
|
|
||
| $li.dataset.documentId = id; | ||
| $li.className = "node-item"; | ||
|
|
||
| $li.innerHTML = ` | ||
| <section class="tree-node-li"> | ||
| <div class="node-title"> | ||
| <span class="material-symbols-outlined">keyboard_arrow_right</span> | ||
| <span>${title}</span> | ||
| </div> | ||
| <div class="node-button-group"> | ||
| <button class="${deleteButton}" data-parent-id="${id}"> | ||
| <span class="material-symbols-outlined"> | ||
| more_horiz | ||
| </span> | ||
| </button> | ||
| <button class="${addButton}" data-parent-id="${id}"> | ||
| <span class="material-symbols-outlined"> | ||
| add | ||
| </span> | ||
| </button> | ||
| </div> | ||
| </section> | ||
| `; | ||
|
|
||
| if (documents.length > 0) { | ||
| this.documentNode = new DocumentNode({ | ||
| $target: $li, | ||
| initialState: { | ||
| data: documents, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| return $li.outerHTML; | ||
| }) | ||
| .join("")} | ||
| </ul> | ||
| `; | ||
| }; | ||
|
|
||
| this.render(); | ||
|
|
||
| const onClickButton = ($button) => { | ||
| const { | ||
| className, | ||
| dataset: { parentId }, | ||
| } = $button; | ||
|
|
||
| if (!parentId) { | ||
| return; | ||
| } | ||
|
|
||
| if (className === addButton) { | ||
| onCreate(parseInt(parentId)); | ||
| return; | ||
| } | ||
|
|
||
| if (className === deleteButton) { | ||
| onDelete(parentId); | ||
| return; | ||
| } | ||
|
yezyvibe marked this conversation as resolved.
|
||
| }; | ||
|
|
||
| const onSelectDocument = ($li) => { | ||
| const { documentId } = $li.dataset; | ||
|
|
||
| if (documentId) { | ||
| navigate(`/documents/${documentId}`); | ||
| } | ||
| }; | ||
|
|
||
| $node.addEventListener("click", (e) => { | ||
| const $button = e.target.closest("button"); | ||
| if ($button) { | ||
| onClickButton($button); | ||
| return; | ||
| } | ||
|
|
||
| const $li = e.target.closest("li"); | ||
| if ($li) { | ||
| onSelectDocument($li); | ||
| return; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| export default DocumentNode; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.