-
Notifications
You must be signed in to change notification settings - Fork 28
[Mission4/김유리] - Project_Notion_VanillaJS #17
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
glassk
wants to merge
41
commits into
prgrms-fe-devcourse:3/#4_kimyuri
Choose a base branch
from
glassk:3/#4_kimyuri_working
base: 3/#4_kimyuri
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 29 commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
cb14e8e
chore: 폴더 구조 세팅, npm 스크립트 추가
glassk cc2d8f2
feat: Sidebar, DocumentList 컴포넌트 구현
glassk 2b4cbc9
feat: DocumentEditPage, Editor 컴포넌트 구현
glassk b6a5123
feat: title이 없는 document Untitled로 표기
glassk ac3673c
feat: Document 생성 request body 형식 변경
glassk 3828078
design: 컴포넌트 기본 스타일링
glassk 0b18965
refactor: Documents fetch 호출 형식 변경
glassk a234e3f
feat: 글 제목과 document.title 동기화
glassk ce9007d
rename: constants.js
glassk e762025
feat: DocumentList 조회, 삭제, 추가 기능 구현
glassk 0a89db0
feat: 루트 문서 추가 버튼 구현
glassk 3f76150
feat: DocumentHeader 컴포넌트 구현
glassk cade10b
feat: DocumentList 문서 제목 ellipsis 처리
glassk 91bd0a6
refactor: App 컴포넌트로 핸들러 분리
glassk f1ea665
fix: 하위 문서 수정 시 전달되는 setState 인자 수정
glassk b8e6874
fix: 스토리지에 동일한 id가 중복 저장되는 에러 해결
glassk e096a39
feat: DocumentFooter 컴포넌트 구현
glassk 454af02
feat: 404 Error, 뒤로가기 처리
glassk 837e14e
feat: Sidebar Header와 스크롤바 추가
glassk 43b4583
feat: 문서 목록에서 선택된 문서 하이라이트 처리
glassk 5abc075
feat: storage id 변경
glassk 8a16df8
fix: 현재 페이지의 하위 페이지 삭제 시 DocumentFooter 업데이트
glassk be54a9b
feat: 아이콘 툴팁
glassk c5506e9
refactor: generateTitle 함수 분리
glassk 0739c20
feat: 페이지 추가 버튼 DocumentList에서 렌더링하도록 수정
glassk 9bf41b3
rename: components 구조 변경
glassk 17d8cd2
chore: npm 스크립트 명령어 수정
glassk d62ae8e
design: Sidebar 스타일 수정
glassk 05c414a
docs: README.md 수정
glassk 320b486
chore: deploy
glassk 94b90f3
fix: api endpoint
glassk f3e9c9d
chore: deploy 방식 변경
glassk 27e4bf5
refactor: 열린 토글 목록 관리 코드 개선
glassk 56c2448
refactor: 문서 삭제 핸들러 인자 변경
glassk eb5b0a4
fix: 문서 편집 중에도 focusout되는 에러 해결
glassk 4480d22
refactor: storage 메서드명 변경
glassk 6631da8
refactor: 문자열 변수로 변경
glassk 81a9ddd
refactor: route url 생성 메서드 분리
glassk 7c67f9a
fix: 사이드바에 커서 올렸을 때 움직이는 현상 해결
glassk aa363d3
docs: README.md 수정
glassk 3ee56a5
docs: README.md 수정
glassk 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
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,60 @@ | ||
| # 📌 4주차 프로젝트[Project1] | ||
|
|
||
| ## 필수 프로젝트 | ||
|
|
||
| - 프로젝트 기한 | ||
| - 프로젝트 수행 기간 : 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 [프로젝트] 노션 클로닝 요구사항** 확인 부탁드립니다. | ||
|
|
||
|
|
||
| ## 📌 과제 설명 | ||
|
|
||
| - VanillaJS로 [노션(Notion)](https://www.notion.so/ko-kr)을 클로닝했습니다. | ||
|
|
||
| ### 실행 방법 | ||
| 1. /src/utils/api.js의 1라인에서 `API_END_POINT` 값을 설정해 주세요. | ||
| 2. 루트 디렉토리에서 터미널로 `npm start` 또는 `npx serve -s`를 실행하여 개발 서버를 띄울 수 있습니다. | ||
|
|
||
| ### 디렉토리 구조 | ||
| <img width="30%" src="https://user-images.githubusercontent.com/63575891/202363329-abd996ae-f330-4bee-81a7-05cb50a3ed8b.png"> | ||
|
|
||
| ## 👩💻 요구 사항과 구현 내용 | ||
|
|
||
| - 화면 좌측에 루트 Documents를 불러오는 API를 통해 루트 Documents를 렌더링합니다. | ||
| - 루트 Document를 클릭하면 오른쪽 편집기 영역에 해당 Document를 렌더링합니다. | ||
| - 해당 루트 Document에 하위 Document가 있는 경우 해당 Document 아래에 트리 형태로 렌더링합니다. | ||
| - Document Tree에서 각 Document 우측의 `+` 버튼을 클릭하면, 클릭한 Document의 하위 Document로 새 Document를 생성하고 편집화면으로 넘깁니다. | ||
| - 열린 Document 목록의 documentId를 localStorage에 저장하여 새로고침해도 열린 상태를 유지합니다. | ||
| - 현재 접속 중인 Document를 하이라이트합니다. | ||
| - 루트 Documents나 Sidebar 하단의 버튼을 클릭하여 루트 Document를 추가합니다. | ||
| - Document 제목이 긴 경우 생략 처리(`...`)합니다. | ||
| - Document Save API를 이용해 지속적으로 서버에 저장합니다. | ||
| - Document의 제목을 수정하면 Sidebar와 Document.title에 반영됩니다. | ||
| - History API를 이용해 SPA 형태로 만듭니다. | ||
| - 루트 URL 접속 시 별도의 편집기를 선택하지 않습니다. | ||
| - `/documents/{documentId}` 로 접속 시 해당 Document를 불러와 편집기에 로딩합니다. | ||
| - 존재하지 않는 documentId로 접속 시 첫 Document를 렌더링합니다. | ||
| - 편집기 최하단에는 현재 편집 중인 Document의 하위 Document를 렌더링합니다. | ||
| - 삭제(휴지통) 버튼을 클릭하면 Document를 삭제합니다. | ||
| - 현재 편집 중인 Document를 삭제하면 첫 Document를 렌더링하고 Document Tree를 변화합니다. | ||
| - 현재 편집 중이 아닌 Document를 삭제하면 Document Tree만 변화합니다. | ||
|
|
||
| ### 루트 Document 추가 및 수정 | ||
|
|
||
| <img src="https://user-images.githubusercontent.com/63575891/202364444-b1e0a2b8-8d51-4718-afc0-aeaea15bbca2.gif" /> | ||
|
|
||
| ### 하위 Document 추가 및 수정 | ||
|
|
||
| <img src="https://user-images.githubusercontent.com/63575891/202364754-470346e2-fce6-473b-9a05-0fe42d68705c.gif" /> | ||
|
|
||
| ### Document 삭제 | ||
|
|
||
| <img src="https://user-images.githubusercontent.com/63575891/202365057-a118535a-a4ea-49b3-b4d7-598c3d73db0f.gif" /> | ||
|
|
||
| ### 존재하지 않은 Document 접속 | ||
|
|
||
| <img src="https://user-images.githubusercontent.com/63575891/202364872-e6764c3c-163f-425d-91c0-6cd6e38f4b12.gif" /> | ||
|
|
||
| ### 현재 편집 중인 Document의 하위 Document 렌더링 | ||
|
|
||
| <img src="https://user-images.githubusercontent.com/63575891/202368323-26148d38-93d6-4fed-b6ce-d322cab10689.gif" /> |
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="ko"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>Notion</title> | ||
| <link rel="stylesheet" href="/src/styles/reset.css"> | ||
| <link rel="stylesheet" href="/src/styles/style.css"> | ||
| <script src="https://kit.fontawesome.com/0ab4707042.js" crossorigin="anonymous"></script> | ||
| </head> | ||
| <body> | ||
| <main id="app"></main> | ||
| <script src="/src/main.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,14 @@ | ||
| { | ||
| "name": "fedc3-4_project_notion_vanillajs", | ||
| "version": "1.0.0", | ||
| "description": "Notion 클로닝", | ||
| "scripts": { | ||
| "start": "npx serve -s" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/prgrms-fe-devcourse/FEDC3-4_Project_Notion_VanillaJS.git" | ||
| }, | ||
| "author": "yurikim", | ||
| "license": "ISC" | ||
| } |
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,146 @@ | ||
| import Sidebar from './Sidebar/Sidebar.js'; | ||
| import DocumentEditPage from './DocumentEditPage/DocumentEditPage.js'; | ||
|
|
||
| import { | ||
| DEFAULT_DOCUMENT_ID, | ||
| NEW, | ||
| NEW_PARENT, | ||
| OPENED_ITEMS, | ||
| ROUTE_DOCUMENTS, | ||
| } from '../utils/constants.js'; | ||
| import { isNew, setDocumentTitle } from '../utils/helper.js'; | ||
| import { initRouter, push } from '../utils/router.js'; | ||
| import { fetchDocuments } from '../utils/api.js'; | ||
| import { getItem, removeItem, setItem } from '../utils/storage.js'; | ||
|
|
||
| export default function App({ $target }) { | ||
| isNew(new.target); | ||
|
|
||
| let timer = null; | ||
|
|
||
| const onAdd = async () => { | ||
| push(`${ROUTE_DOCUMENTS}/${NEW}`); | ||
|
|
||
| const createdDocument = await fetchDocuments('', { | ||
| method: 'POST', | ||
| body: JSON.stringify({ | ||
| title: '', | ||
| parent: getItem(NEW_PARENT, null), | ||
| }), | ||
| }); | ||
|
|
||
| history.replaceState( | ||
| null, | ||
| null, | ||
| `${ROUTE_DOCUMENTS}/${createdDocument.id}` | ||
| ); | ||
| removeItem(NEW_PARENT); | ||
|
|
||
| documentEditPage.setState({ documentId: createdDocument.id }); | ||
|
|
||
| sidebar.setState({ | ||
| selectedId: parseInt(createdDocument.id), | ||
| }); | ||
| }; | ||
|
|
||
| const onDelete = async (documentId) => { | ||
| if (documentId === DEFAULT_DOCUMENT_ID) { | ||
| alert('첫 페이지는 지우지 말아주세요 :D'); | ||
| return; | ||
| } | ||
|
|
||
| if (!confirm('페이지를 삭제하시겠습니까?')) return; | ||
glassk marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| await fetchDocuments(documentId, { | ||
| method: 'DELETE', | ||
| }); | ||
|
|
||
| const openedItems = getItem(OPENED_ITEMS, []); | ||
| const index = openedItems.indexOf(documentId); | ||
| if (index > -1) { | ||
| setItem(OPENED_ITEMS, [ | ||
| ...openedItems.slice(0, index), | ||
| ...openedItems.slice(index + 1), | ||
| ]); | ||
| } | ||
glassk marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const currentId = documentEditPage.state.documentId; | ||
glassk marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (currentId === documentId) { | ||
| documentEditPage.setState({ documentId: DEFAULT_DOCUMENT_ID }); | ||
| push(`${ROUTE_DOCUMENTS}/${DEFAULT_DOCUMENT_ID}`); | ||
| } else { | ||
| documentEditPage.setState({ documentId: currentId }); | ||
| } | ||
|
|
||
| sidebar.render(); | ||
| }; | ||
|
|
||
| const onEdit = ({ id, title, content }) => { | ||
| if (timer !== null) { | ||
| clearTimeout(timer); | ||
| } | ||
| timer = setTimeout(async () => { | ||
| const editedDocument = await fetchDocuments(id, { | ||
| method: 'PUT', | ||
| body: JSON.stringify({ title, content }), | ||
| }); | ||
|
|
||
| documentEditPage.setState({ | ||
| documentId: editedDocument.id, | ||
| document: editedDocument, | ||
| }); | ||
|
|
||
| sidebar.render(); | ||
| }, 1000); | ||
| }; | ||
|
|
||
| const sidebar = new Sidebar({ | ||
| $target, | ||
| initialState: { | ||
| selectedId: null, | ||
| }, | ||
| onAdd, | ||
| onDelete, | ||
| }); | ||
|
|
||
| const documentEditPage = new DocumentEditPage({ | ||
| $target, | ||
| initialState: { | ||
| documentId: null, | ||
| document: { | ||
| title: '', | ||
| content: '', | ||
| }, | ||
| }, | ||
| onDelete, | ||
| onEdit, | ||
| }); | ||
|
|
||
| this.route = () => { | ||
| const { pathname } = window.location; | ||
|
|
||
| if (pathname === '/') { | ||
| setDocumentTitle('Notion'); | ||
| return; | ||
| } | ||
|
|
||
| if (pathname.indexOf(ROUTE_DOCUMENTS) !== 0) return; | ||
|
|
||
| const [, , documentId] = pathname.split('/'); | ||
| documentEditPage.setState({ | ||
| documentId: isNaN(documentId) ? documentId : parseInt(documentId), | ||
| }); | ||
|
|
||
| if (!isNaN(documentId)) { | ||
| sidebar.setState({ | ||
| selectedId: parseInt(documentId), | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| window.addEventListener('popstate', () => this.route()); | ||
|
|
||
| this.route(); | ||
|
|
||
| initRouter(() => this.route()); | ||
| } | ||
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,105 @@ | ||
| import Editor from './Editor.js'; | ||
| import DocumentHeader from './DocumentHeader.js'; | ||
| import DocumentFooter from './DocumentFooter.js'; | ||
|
|
||
| import { fetchDocuments } from '../../utils/api.js'; | ||
| import { | ||
| NEW, | ||
| ROUTE_DOCUMENTS, | ||
| DEFAULT_DOCUMENT_ID, | ||
| } from '../../utils/constants.js'; | ||
| import { isNew, setDocumentTitle } from '../../utils/helper.js'; | ||
| import { push } from '../../utils/router.js'; | ||
|
|
||
| export default function DocumentEditPage({ | ||
| $target, | ||
| initialState, | ||
| onDelete, | ||
| onEdit, | ||
| }) { | ||
| isNew(new.target); | ||
|
|
||
| const $page = document.createElement('div'); | ||
| $page.className = 'document-edit-page'; | ||
|
|
||
| this.state = initialState; | ||
|
|
||
| const documentHeader = new DocumentHeader({ | ||
| $target: $page, | ||
| initialState: this.state, | ||
| onDelete, | ||
| }); | ||
|
|
||
| const editor = new Editor({ | ||
| $target: $page, | ||
| initialState: { | ||
| title: '', | ||
| content: '', | ||
| }, | ||
| onEdit, | ||
| }); | ||
|
|
||
| const documentFooter = new DocumentFooter({ | ||
| $target: $page, | ||
| initialState: { | ||
| document: null, | ||
| }, | ||
| }); | ||
|
|
||
| this.setState = async (nextState) => { | ||
| if (this.state.documentId === nextState.documentId && nextState.document) { | ||
| this.state = { ...this.state, ...nextState }; | ||
| editor.setState( | ||
| this.state.document || { | ||
| title: '', | ||
| content: '', | ||
| } | ||
| ); | ||
| documentHeader.setState(this.state); | ||
| documentFooter.setState({ | ||
| document: this.state.document, | ||
| }); | ||
| this.render(); | ||
| return; | ||
| } | ||
|
|
||
| this.state = { ...this.state, ...nextState }; | ||
|
|
||
| if (this.state.documentId === NEW) { | ||
| editor.setState({ | ||
| title: '', | ||
| content: '', | ||
| }); | ||
| documentHeader.setState(this.state); | ||
| documentFooter.setState({ | ||
| document: null, | ||
| }); | ||
| this.render(); | ||
| } else { | ||
| await loadDocument(); | ||
| } | ||
| }; | ||
|
|
||
| const loadDocument = async () => { | ||
| const document = await fetchDocuments(this.state.documentId); | ||
| if (!document) { | ||
| alert('존재하지 않는 페이지입니다. 첫 페이지로 이동합니다.'); | ||
| push(`${ROUTE_DOCUMENTS}/${DEFAULT_DOCUMENT_ID}`); | ||
| return; | ||
| } | ||
|
|
||
| this.setState({ | ||
| ...this.state, | ||
| document, | ||
| }); | ||
| documentFooter.setState({ | ||
| document: this.state.document, | ||
| }); | ||
| setDocumentTitle(this.state.document?.title || ''); | ||
| }; | ||
|
|
||
| this.render = () => { | ||
| $target.appendChild($page); | ||
glassk marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| setDocumentTitle(this.state.document?.title || ''); | ||
| }; | ||
| } | ||
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,44 @@ | ||
| import { ROUTE_DOCUMENTS } from '../../utils/constants.js'; | ||
| import { push } from '../../utils/router.js'; | ||
| import { generateTitle } from '../../utils/helper.js'; | ||
|
|
||
| export default function DocumentFooter({ $target, initialState }) { | ||
| const $footer = document.createElement('footer'); | ||
| $footer.className = 'document-footer'; | ||
|
|
||
| $target.appendChild($footer); | ||
|
|
||
| this.state = initialState; | ||
|
|
||
| this.setState = (nextState) => { | ||
| this.state = { ...this.state, ...nextState }; | ||
| this.render(); | ||
| }; | ||
|
|
||
| this.render = () => { | ||
| if (!this.state.document || !this.state.document.documents) return; | ||
|
|
||
| const { documents } = this.state.document; | ||
|
|
||
| $footer.innerHTML = ` | ||
| <div class="titles"> | ||
| ${documents | ||
| .map( | ||
| ({ id, title }) => | ||
| `<p data-id="${id}" class="title">${generateTitle(title)}</p>` | ||
| ) | ||
| .join('')} | ||
| </div> | ||
| `; | ||
| }; | ||
|
|
||
| $footer.addEventListener('click', (e) => { | ||
| const $title = e.target.closest('.title'); | ||
| if (!$title) return; | ||
|
|
||
| const { id } = $title.dataset; | ||
| push(`${ROUTE_DOCUMENTS}/${parseInt(id)}`); | ||
| }); | ||
|
|
||
| this.render(); | ||
| } |
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.