├── .prettierrc ├── README.md ├── docs ├── demo.gif └── structure.jpeg ├── index.html ├── src ├── App.js ├── api │ ├── config │ │ └── index.js │ └── notion.js ├── assets │ ├── css │ │ ├── fontello.css │ │ ├── index.css │ │ └── reset.css │ ├── font │ │ ├── fontello │ │ │ ├── fontello.eot │ │ │ ├── fontello.svg │ │ │ ├── fontello.ttf │ │ │ ├── fontello.woff │ │ │ └── fontello.woff2 │ │ └── opensans │ │ │ ├── OpenSans-Bold.ttf │ │ │ ├── OpenSans-ExtraBold.ttf │ │ │ └── OpenSans-Regular.ttf │ └── images │ │ ├── 404.png │ │ └── index.png ├── components │ ├── modal │ │ ├── Modal.js │ │ ├── ModalBody.js │ │ └── ModalHeader.js │ ├── posts │ │ ├── PostsPage.js │ │ ├── PostsPageBody.js │ │ └── PostsPageNoData.js │ └── sidebar │ │ ├── Sidebar.js │ │ ├── SidebarBody.js │ │ ├── SidebarFooter.js │ │ └── SidebarHeader.js ├── main.js ├── pages │ ├── MainPage.js │ └── NotFoundPage.js ├── store │ ├── gettersLi.js │ ├── gettersState.js │ ├── index.js │ └── settersElement.js └── utils │ ├── emitter.js │ ├── render.js │ ├── selector.js │ ├── storage.js │ ├── templates.js │ └── valid.js └── vercel.json /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "useTabs": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 80, 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🌐 사이트 주소 2 | https://surim-notion.vercel.app/ 3 | 4 | ## 🎥 시연 영상 5 | 6 | ![demo](https://user-images.githubusercontent.com/47546413/132133257-d0abc8cb-35d0-4de8-925a-5059ae586953.gif) 7 | 8 | 12 | 13 | ## 📌 프로젝트 설명 14 | 15 | ### 앱 구조 16 | 17 | ![structure](https://user-images.githubusercontent.com/47546413/132133273-5a44bbb5-d835-4f9b-baa9-288e8a7bce57.jpeg) 18 | 19 | 컴포넌트들을 세분화 하여 이벤트 관리가 복잡하다고 느껴졌습니다.. 20 | 이벤트와 로직을 어떻게 하면 효율적으로 관리할 수 있을지 고민을 하였고, 21 | 리액트의 상태 관리 방법을 찾기 위해 `redux`를 찾아보았습니다. 하지만 사용해보지 않았던 개념이라 파악이 어려웠습니다.. 22 | 짧게나마 사용해보았던 `vuex` 원리에 착안하여 미약하게나마 `store` 형태를 흉내 내 보았습니다.. 23 | 위와 같이 구현 한 이유는 `store`를 이용해서 한 군데에서 이벤트와 로직을 관리하면 좋겠다 생각하여 해당 구조를 생각하게 되었습니다. 24 | 25 | ### 데이터 흐름 26 | 27 | 1. `emit` **하위 컴포넌트**에서 이벤트가 발생합니다. ( Child → Store) 28 | 2. `on` **Store**에서 이벤트를 감지합니다. ( Store ← Child) 29 | 3. `dispatch` gettersStore를 이용하여 해당하는 로직을 실행한 뒤, 결과가 반영된 데이터를 가져옵니다. (Store) 30 | 4. `commit` 변경된 데이터와 렌더링이 필요한 내용을 담아 **App 컴포넌트**에 전달합니다. ( Store → App) 31 | - APP 컴포넌트의 데이터는 `commit`으로만 변경이 가능합니다. 32 | 5. 비동기 통신 이후 처리될 렌더링관련 로직은 `settesrElement`에서 실행됩니다. 33 | 6. Store에서 전달받은 내용으로 **APP 컴포넌트**에서 `state`와 `needRender(렌더링이 필요한 컴포넌트)` 하위 컴포넌트로 전달합니다. (App → Child) 34 | 7. `state`는 모두 업데이트되지만 `needRender`에 의해 필요한 부분만 다시 렌더링이 됩니다. (App → Child) 35 | 36 | ### 디렉토리 구조와 역할 37 | 38 | - `main.js` : root에 App을 생성 39 | - `App.js`: 최상위 컴포넌트 40 | - route 처리 41 | - Store에서 데이터(nextState, needRender)를 받은 뒤, 하위 컴포넌트들에게 전달 42 | - `api` : api관련 js 파일 43 | - `config` : api기본 설정 파일 44 | - `notion` : notion api 파일 45 | - `assets` : style에 필요한 요소 46 | - `css` : css관련 파일 (index, reset, fontello) 47 | - `font` : 사용된 font (openSans, fontello) 48 | - `images` : 페이지 내 이미지 (index, 404) 49 | - `components` : 페이지 내 하위 컴포넌트 50 | - `modal` 51 | - `Modal.js` : Modal 하위 요소 생성, 동기적인 UI 로직 실행 → Store에 이벤트 emit 52 | - `ModalBody.js` : Modal의 Title,Content를 렌더링 53 | - `ModalHeader.js` : Modal 상단의 페이지로 열기, x 버튼 렌더링 54 | - `posts` 55 | - `PostsPage.js` : Posts 하위 요소 생성, 동기적인 UI 로직 실행 → Store에 이벤트 emit 56 | - `PostsPageBody.js` : document 렌더링 57 | - `PostsPageNoData.js` : index 렌더링 58 | - `sidebar` 59 | - `Sidebar.js` : Sidebar 하위 요소 생성, 동기적인 UI 로직 실행 → Store에 이벤트 emit 60 | - `SidebarHeader.js` : notion 타이틀 렌더링 61 | - `SidebarBody.js`: document 트리(navlist) 렌더링 62 | - `SidebarFooter.js` : 새 페이지 버튼 렌더링 63 | - `pages` : 페이지 컴포넌트 64 | - `MainPage.js`: 하위 컴포넌트 (Sidebar, posts, modal)들을 가지고 있는 컴포넌트 65 | - `NotFoundPage.js`: 404 Error 페이지 컴포넌트 66 | - `store` : App 컴포넌트 상태 저장소 67 | - `index.js` 68 | - `APP`컴포넌트의 상태 변경 (commit) 69 | - 하위 컴포넌트의 이벤트 감지 후 비동기 작업 진행 (dispatch) 70 | - `gettersLi.js` : openedLi의 데이터를 가공하여 반환 71 | - `gettersState.js` : state의 데이터를 가공하여 반환 72 | - `settersElement.js` : 가공된 state를 이용하여 비동기적인 UI 로직 실행 73 | - `utils` : 유틸 함수 74 | - `emitter.js`: on과 emit으로 이벤트를 관리 75 | - on : 이벤트를 바인딩 (감지) 76 | - emit: 이벤트를 실행 77 | - `render.js` : 렌더링에 필요한 함수 (Ex. 태그의 스타일, 컨텐츠 변경 등) 78 | - `selector.js`: 전역으로 사용되는 element 선택관련 함수 79 | - `templates.js`: 컨텐츠 없는 template 태그를 생성 80 | - `storage.js`: 스토리지 get, set 함수 81 | - `valid.js`: getters의 valid를 체크 82 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonsurim/surim-notion/3009d7784831d4e06d45b6a6e79c91619685f21c/docs/demo.gif -------------------------------------------------------------------------------- /docs/structure.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonsurim/surim-notion/3009d7784831d4e06d45b6a6e79c91619685f21c/docs/structure.jpeg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 손수림의 notion 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { on } from './utils/emitter.js'; 2 | import { getStateAfter } from './store/gettersState.js'; 3 | 4 | import Store from './store/index.js'; 5 | import NotFoundPage from './pages/NotFoundPage.js'; 6 | import MainPage from './pages/MainPage.js'; 7 | 8 | export default function App({ $target }) { 9 | this.init = async () => { 10 | new Store(); 11 | 12 | const notFoundPage = new NotFoundPage({ $target }); 13 | const mainpage = new MainPage({ $target, initialState: {} }); 14 | 15 | this.setState = ({ nextState, needRender }) => { 16 | this.state = nextState; 17 | mainpage.setState({ nextState, needRender }); 18 | }; 19 | 20 | this.route = async () => { 21 | const { pathname } = window.location; 22 | 23 | if (pathname === '/404') { 24 | notFoundPage.render(); 25 | return; 26 | } 27 | 28 | const nextState = await getStateAfter('fetch'); 29 | this.setState({ nextState, needRender: 'all' }); 30 | }; 31 | this.route(); 32 | 33 | on.initStore((nextState, needRender) => 34 | this.setState({ nextState, needRender }), 35 | ); 36 | on.initRouter(() => this.route()); 37 | }; 38 | 39 | this.init(); 40 | } 41 | -------------------------------------------------------------------------------- /src/api/config/index.js: -------------------------------------------------------------------------------- 1 | const BASE_URL = 'https://kdt.roto.codes/documents'; 2 | 3 | const request = async (url, options = {}) => { 4 | try { 5 | const res = await fetch(`${BASE_URL}${url}`, { 6 | ...options, 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | 'x-username': 'sonsurim', 10 | }, 11 | }); 12 | 13 | if (res.ok) { 14 | return await res.json(); 15 | } 16 | 17 | throw new Error('API 처리 중 오류가 발생하였습니다!'); 18 | } catch (e) { 19 | console.log(e.message); 20 | history.replaceState(null, null, '/404'); 21 | window.location = `${window.location.origin}/404`; 22 | } 23 | }; 24 | 25 | export { request }; 26 | -------------------------------------------------------------------------------- /src/api/notion.js: -------------------------------------------------------------------------------- 1 | import { request } from './config/index.js'; 2 | 3 | const getDocuments = async id => { 4 | const url = id ? `/${id}` : ''; 5 | return await request(url, { method: 'GET' }); 6 | }; 7 | 8 | const createDocument = async document => { 9 | return await request('', { 10 | method: 'POST', 11 | body: JSON.stringify(document), 12 | }); 13 | }; 14 | 15 | const updateDocument = async (id, document) => { 16 | return await request(`/${id}`, { 17 | method: 'PUT', 18 | body: JSON.stringify(document), 19 | }); 20 | }; 21 | 22 | const deleteDocument = async id => { 23 | return await request(`/${id}`, { 24 | method: 'DELETE', 25 | }); 26 | }; 27 | 28 | export { getDocuments, createDocument, updateDocument, deleteDocument }; 29 | -------------------------------------------------------------------------------- /src/assets/css/fontello.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'fontello'; 3 | src: url('../font/fontello/fontello.eot?32065777'); 4 | src: url('../font/fontello/fontello.eot?32065777#iefix') 5 | format('embedded-opentype'), 6 | url('../font/fontello/fontello.woff2?32065777') format('woff2'), 7 | url('../font/fontello/fontello.woff?32065777') format('woff'), 8 | url('../font/fontello/fontello.ttf?32065777') format('truetype'), 9 | url('../font/fontello/fontello.svg?32065777#fontello') format('svg'); 10 | font-weight: normal; 11 | font-style: normal; 12 | } 13 | [class^='icon-']:before, 14 | [class*=' icon-']:before { 15 | font-family: 'fontello'; 16 | } 17 | 18 | .icon-play:before { 19 | content: '\e800'; 20 | } 21 | .icon-down-dir:before { 22 | content: '\e801'; 23 | } 24 | .icon-right-dir:before { 25 | content: '\e802'; 26 | } 27 | .icon-trash-empty:before { 28 | content: '\e803'; 29 | } 30 | .icon-resize-full:before { 31 | content: '\e804'; 32 | } 33 | .icon-cancel:before { 34 | content: '\e805'; 35 | } 36 | .icon-plus-squared-alt:before { 37 | content: '\f196'; 38 | } 39 | -------------------------------------------------------------------------------- /src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 10px; 3 | overflow-x: hidden; 4 | font-family: 'Open Sans', sans-serif; 5 | box-sizing: content-box; 6 | } 7 | body { 8 | overflow-x: hidden; 9 | } 10 | input, 11 | textarea, 12 | button { 13 | font-family: 'Open Sans', sans-serif; 14 | border: none; 15 | resize: none; 16 | } 17 | button { 18 | cursor: pointer; 19 | padding: none; 20 | background-color: #1fe0; 21 | } 22 | 23 | /* layout */ 24 | .row { 25 | display: flex; 26 | height: 100vh; 27 | } 28 | 29 | /* sidebar */ 30 | .sidebar-container { 31 | position: relative; 32 | flex-grow: 0; 33 | flex-shrink: 0; 34 | width: 240px; 35 | background: rgb(247, 246, 243); 36 | color: rgba(25, 23, 17, 0.6); 37 | font-size: 1.5rem; 38 | } 39 | .header-title { 40 | display: flex; 41 | align-items: center; 42 | padding: 0.2rem 1.6rem; 43 | width: 100%; 44 | height: 45px; 45 | font-weight: 600; 46 | font-size: 1.4rem; 47 | color: rgb(55, 53, 47); 48 | } 49 | .nav-container { 50 | overflow-y: scroll; 51 | height: 85vh; 52 | } 53 | .nav-list { 54 | position: relative; 55 | } 56 | .nav-item { 57 | display: flex; 58 | align-items: center; 59 | cursor: pointer; 60 | padding: 0 15px; 61 | min-height: 3rem; 62 | font-size: 1.4rem; 63 | } 64 | .root { 65 | overflow: hidden; 66 | } 67 | .tree { 68 | padding-left: 30px; 69 | } 70 | .tree p { 71 | width: 240px; 72 | } 73 | .item-container { 74 | position: absolute; 75 | left: 0; 76 | width: 240px; 77 | height: 3.1rem; 78 | } 79 | p.blank { 80 | margin-left: 35px; 81 | padding: 8px; 82 | font-size: 1.4rem; 83 | color: rgba(55, 53, 47, 0.4); 84 | } 85 | .nav-item.selected { 86 | color: rgb(55, 53, 47); 87 | font-weight: 700; 88 | } 89 | .nav-item:hover .item-container { 90 | background: rgba(55, 53, 47, 0.08); 91 | } 92 | .nav-item.selected .item-container { 93 | background: rgba(55, 53, 47, 0.08); 94 | } 95 | .nav-toggler-btn.icon-down-dir { 96 | font-size: 1.7rem; 97 | margin-top: 0.2rem; 98 | } 99 | .nav-toggler-btn.icon-play { 100 | font-size: 1.1rem; 101 | margin-top: -0.2rem; 102 | } 103 | .nav-page-title { 104 | user-select: none; 105 | text-overflow: ellipsis; 106 | overflow: hidden; 107 | white-space: nowrap; 108 | z-index: 2; 109 | } 110 | .nav-toggler-btn, 111 | .nav-create-btn, 112 | .nav-delete-btn { 113 | user-select: none; 114 | opacity: 0.5; 115 | border-radius: 3px; 116 | z-index: 2; 117 | } 118 | .nav-delete-btn { 119 | display: none; 120 | position: absolute; 121 | padding: 0.5rem 1rem; 122 | right: 3rem; 123 | color: #7c7c7a; 124 | background: #e8e7e4; 125 | opacity: 1; 126 | } 127 | .nav-create-btn { 128 | display: none; 129 | position: absolute; 130 | padding: 0.5rem 1rem; 131 | right: 0; 132 | color: #7c7c7a; 133 | background: #e8e7e4; 134 | opacity: 1; 135 | } 136 | .nav-toggler-btn { 137 | padding: 0.5rem 1rem; 138 | } 139 | .nav-toggler-btn:hover { 140 | display: block; 141 | background: #dad9d6; 142 | } 143 | .nav-create-btn:hover { 144 | display: block; 145 | background: #dad9d6; 146 | } 147 | .nav-delete-btn:hover { 148 | background: #dad9d6; 149 | } 150 | .sidebar-body .create-btn { 151 | cursor: pointer; 152 | padding: 1rem 1.5rem; 153 | } 154 | .sidebar-footer { 155 | box-shadow: rgb(55 53 47 / 9%) 0px -1px 0px; 156 | cursor: pointer; 157 | position: fixed; 158 | bottom: 0; 159 | padding: 2rem 0; 160 | width: 240px; 161 | color: rgba(55, 53, 47, 0.6); 162 | z-index: 3; 163 | background: #f7f6f3; 164 | } 165 | .sidebar-footer:hover { 166 | background: #e8e7e4; 167 | } 168 | .sidebar-footer .create-btn { 169 | padding: 2rem; 170 | user-select: none; 171 | } 172 | 173 | /* page */ 174 | .page-container { 175 | display: flex; 176 | flex-direction: row; 177 | justify-content: center; 178 | padding-top: 7rem; 179 | width: 1200px; 180 | background: white; 181 | } 182 | .page-body { 183 | width: 70%; 184 | } 185 | .page-title { 186 | position: relative; 187 | width: 100%; 188 | } 189 | .show-page-title { 190 | position: relative; 191 | cursor: text; 192 | font-size: 4rem; 193 | font-weight: 700; 194 | line-height: 6rem; 195 | outline: none; 196 | z-index: 1; 197 | } 198 | .hidden-page-title { 199 | position: absolute; 200 | top: 0; 201 | font-size: 4rem; 202 | font-weight: 700; 203 | line-height: 6rem; 204 | color: #ccc; 205 | outline: none; 206 | z-index: 0; 207 | } 208 | .page-content { 209 | margin-top: 5vh; 210 | cursor: text; 211 | } 212 | .show-page-content { 213 | width: 100%; 214 | min-height: 70vh; 215 | font-size: 1.6rem; 216 | line-height: 1.5; 217 | white-space: pre-wrap; 218 | word-break: break-word; 219 | outline: none; 220 | } 221 | 222 | /* modal */ 223 | .modal-container { 224 | position: absolute; 225 | top: 72px; 226 | left: 72px; 227 | right: 72px; 228 | z-index: 9999; 229 | display: flex; 230 | flex-direction: column; 231 | margin-right: auto; 232 | margin-left: auto; 233 | max-width: 960px; 234 | height: calc(100% - 144px); 235 | border-radius: 3px; 236 | background: white; 237 | box-shadow: rgb(15 15 15 / 5%) 0px 0px 0px 1px, 238 | rgb(15 15 15 / 10%) 0px 5px 10px, rgb(15 15 15 / 20%) 0px 15px 40px; 239 | } 240 | .modal-header { 241 | display: flex; 242 | padding: 1rem; 243 | justify-content: space-between; 244 | } 245 | .modal-openpage-btn { 246 | padding: 1rem; 247 | cursor: pointer; 248 | border-radius: 3px; 249 | font-size: 1.4rem; 250 | line-height: 1.2; 251 | color: rgba(55, 53, 47, 0.6); 252 | } 253 | .modal-openpage-btn:hover { 254 | background: rgba(55, 53, 47, 0.08); 255 | } 256 | .modal-close-btn { 257 | padding: 1rem; 258 | cursor: pointer; 259 | border-radius: 3px; 260 | font-size: 1.4rem; 261 | line-height: 1.2; 262 | color: rgba(55, 53, 47, 0.6); 263 | } 264 | .modal-close-btn:hover { 265 | background: rgba(55, 53, 47, 0.1); 266 | } 267 | .modal-body { 268 | display: flex; 269 | flex-direction: column; 270 | padding: 3rem 8rem; 271 | height: 100%; 272 | overflow: scroll; 273 | } 274 | .modal-title { 275 | position: relative; 276 | } 277 | .show-modal-title { 278 | cursor: text; 279 | font-size: 4rem; 280 | font-weight: 700; 281 | line-height: 6rem; 282 | outline: none; 283 | z-index: 2; 284 | } 285 | .hidden-modal-title { 286 | position: absolute; 287 | top: 0; 288 | font-size: 4rem; 289 | font-weight: 700; 290 | line-height: 6rem; 291 | color: #ccc; 292 | outline: none; 293 | z-index: -1; 294 | } 295 | .modal-content { 296 | margin-top: 5vh; 297 | height: 90%; 298 | cursor: text; 299 | } 300 | .show-modal-content { 301 | width: 100%; 302 | height: 80%; 303 | font-size: 1.6rem; 304 | line-height: 1.5; 305 | white-space: pre-wrap; 306 | word-break: break-word; 307 | outline: none; 308 | } 309 | 310 | /* 404 NotFoundPage */ 311 | .not-found { 312 | display: flex; 313 | flex-direction: column; 314 | align-items: center; 315 | justify-content: center; 316 | min-width: 100vh; 317 | min-height: 100vh; 318 | } 319 | .not-fount-title { 320 | position: relative; 321 | font-size: 7rem; 322 | font-weight: 600; 323 | color: #53534e; 324 | } 325 | .not-found-img { 326 | margin: 5vh; 327 | width: 30vh; 328 | cursor: pointer; 329 | opacity: 1; 330 | } 331 | 332 | /* page - nodata*/ 333 | .nodata-page { 334 | display: flex; 335 | flex-direction: column; 336 | align-items: center; 337 | justify-content: center; 338 | } 339 | .nodata-title { 340 | margin-top: 10vh; 341 | font-size: 5.5rem; 342 | font-weight: 700; 343 | color: #dad9d6; 344 | } 345 | .nodata-img { 346 | margin-top: 5vh; 347 | width: 60vh; 348 | } 349 | /* js-controll */ 350 | .show { 351 | display: block; 352 | } 353 | .hide { 354 | display: none; 355 | } 356 | textarea::placeholder { 357 | color: #ccc; 358 | font-size: 1.6rem; 359 | } 360 | -------------------------------------------------------------------------------- /src/assets/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font-size: 100%; 91 | font: inherit; 92 | vertical-align: baseline; 93 | } 94 | /* HTML5 display-role reset for older browsers */ 95 | article, 96 | aside, 97 | details, 98 | figcaption, 99 | figure, 100 | footer, 101 | header, 102 | hgroup, 103 | menu, 104 | nav, 105 | section { 106 | display: block; 107 | } 108 | body { 109 | line-height: 1; 110 | } 111 | ol, 112 | ul { 113 | list-style: none; 114 | } 115 | blockquote, 116 | q { 117 | quotes: none; 118 | } 119 | blockquote:before, 120 | blockquote:after, 121 | q:before, 122 | q:after { 123 | content: ''; 124 | content: none; 125 | } 126 | table { 127 | border-collapse: collapse; 128 | border-spacing: 0; 129 | } 130 | -------------------------------------------------------------------------------- /src/assets/font/fontello/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonsurim/surim-notion/3009d7784831d4e06d45b6a6e79c91619685f21c/src/assets/font/fontello/fontello.eot -------------------------------------------------------------------------------- /src/assets/font/fontello/fontello.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2021 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/assets/font/fontello/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonsurim/surim-notion/3009d7784831d4e06d45b6a6e79c91619685f21c/src/assets/font/fontello/fontello.ttf -------------------------------------------------------------------------------- /src/assets/font/fontello/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonsurim/surim-notion/3009d7784831d4e06d45b6a6e79c91619685f21c/src/assets/font/fontello/fontello.woff -------------------------------------------------------------------------------- /src/assets/font/fontello/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonsurim/surim-notion/3009d7784831d4e06d45b6a6e79c91619685f21c/src/assets/font/fontello/fontello.woff2 -------------------------------------------------------------------------------- /src/assets/font/opensans/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonsurim/surim-notion/3009d7784831d4e06d45b6a6e79c91619685f21c/src/assets/font/opensans/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/assets/font/opensans/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonsurim/surim-notion/3009d7784831d4e06d45b6a6e79c91619685f21c/src/assets/font/opensans/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /src/assets/font/opensans/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonsurim/surim-notion/3009d7784831d4e06d45b6a6e79c91619685f21c/src/assets/font/opensans/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/assets/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonsurim/surim-notion/3009d7784831d4e06d45b6a6e79c91619685f21c/src/assets/images/404.png -------------------------------------------------------------------------------- /src/assets/images/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonsurim/surim-notion/3009d7784831d4e06d45b6a6e79c91619685f21c/src/assets/images/index.png -------------------------------------------------------------------------------- /src/components/modal/Modal.js: -------------------------------------------------------------------------------- 1 | import { on, emit } from '../../utils/emitter.js'; 2 | import { $createElement } from '../../utils/templates.js'; 3 | import { 4 | setCurrentLi, 5 | markListItemOfId, 6 | checkDataForPlaceholder, 7 | } from '../../utils/render.js'; 8 | 9 | import ModalHeader from './ModalHeader.js'; 10 | import ModalBody from './ModalBody.js'; 11 | 12 | export default function Modal({ $target }) { 13 | const $modal = $createElement('div', '.modal-container', '.hide'); 14 | const $modalHeader = $createElement('div', '.modal-header'); 15 | const $modalBody = $createElement('div', '.modal-body'); 16 | 17 | this.state = { 18 | id: 'new', 19 | title: '', 20 | content: '', 21 | }; 22 | this.setState = nextState => { 23 | this.state = nextState; 24 | }; 25 | 26 | new ModalHeader({ 27 | $target: $modalHeader, 28 | onClick: { 29 | openPage: () => { 30 | const { id } = this.state; 31 | 32 | markListItemOfId(id); 33 | emit.readDocument({ id }); 34 | hideModal(); 35 | }, 36 | closeModal: () => { 37 | hideModal(); 38 | }, 39 | }, 40 | }); 41 | 42 | const modalBody = new ModalBody({ 43 | $target: $modalBody, 44 | onUpdate: { 45 | updateTitle: nextDocument => { 46 | const { id } = this.state; 47 | const { title } = nextDocument; 48 | const $target = $('.show-modal-title'); 49 | 50 | setCurrentLi({ id, title }); 51 | checkDataForPlaceholder({ $target }); 52 | emit.updateDocument({ id, nextDocument, onModal: true }); 53 | }, 54 | updateContent: nextDocument => { 55 | const { id } = this.state; 56 | emit.updateDocument({ id, nextDocument, onModal: true }); 57 | }, 58 | }, 59 | }); 60 | 61 | const showModal = () => { 62 | modalBody.render(); 63 | $modal.classList.remove('hide'); 64 | }; 65 | const hideModal = () => { 66 | $modal.classList.add('hide'); 67 | }; 68 | 69 | this.init = () => { 70 | $modal.appendChild($modalHeader); 71 | $modal.appendChild($modalBody); 72 | $target.appendChild($modal); 73 | 74 | on.showModal(showModal); 75 | on.updateModal(nextState => this.setState(nextState)); 76 | 77 | window.addEventListener('click', e => { 78 | const createBtn = e.target.dataset.target === 'modal'; 79 | const onModal = e.target.className.includes('modal'); 80 | const noData = 81 | !$('.show-modal-title')?.textContent && 82 | !$('.show-modal-content')?.value; 83 | const isHide = $modal.classList.contains('hide'); 84 | const isEmpty = !onModal && !isHide && noData; 85 | 86 | if (createBtn || onModal) { 87 | return; 88 | } 89 | 90 | if (isEmpty) { 91 | emit.deleteEmptyDocument(this.state.id); 92 | } 93 | hideModal(); 94 | }); 95 | }; 96 | 97 | this.init(); 98 | } 99 | -------------------------------------------------------------------------------- /src/components/modal/ModalBody.js: -------------------------------------------------------------------------------- 1 | import { $createElement, $hiddenTitleItem } from '../../utils/templates.js'; 2 | import { setPlaceholderTitle } from '../../utils/render.js'; 3 | 4 | export default function ModalBody({ $target, onUpdate }) { 5 | const $modalTitle = $createElement('p', '.modal-title'); 6 | const $modalContent = $createElement('p', '.modal-content'); 7 | 8 | const $hiddenTitleInput = $hiddenTitleItem('hidden-modal-title'); 9 | const $titleInput = $createElement('div', '.show-modal-title'); 10 | $titleInput.setAttribute('contenteditable', true); 11 | 12 | const $contentInput = $createElement('textarea', '.show-modal-content'); 13 | $contentInput.setAttribute('placeholder', '문서의 내용을 입력해보세요!'); 14 | 15 | this.render = () => { 16 | $titleInput.textContent = ''; 17 | $contentInput.value = ''; 18 | setPlaceholderTitle({ $target: $hiddenTitleInput, title: null }); 19 | }; 20 | 21 | this.init = () => { 22 | this.render(); 23 | 24 | $modalTitle.appendChild($titleInput); 25 | $modalTitle.appendChild($hiddenTitleInput); 26 | $modalContent.appendChild($contentInput); 27 | $target.appendChild($modalTitle); 28 | $target.appendChild($modalContent); 29 | 30 | $titleInput.addEventListener('keyup', e => { 31 | const title = e.target.textContent; 32 | const content = $contentInput.value; 33 | 34 | const nextDocument = { title, content }; 35 | onUpdate.updateTitle(nextDocument); 36 | }); 37 | 38 | $contentInput.addEventListener('keyup', e => { 39 | const title = $titleInput.textContent; 40 | const content = e.target.value; 41 | 42 | const nextDocument = { title, content }; 43 | onUpdate.updateContent(nextDocument); 44 | }); 45 | }; 46 | 47 | this.init(); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/modal/ModalHeader.js: -------------------------------------------------------------------------------- 1 | import { $createElement } from '../../utils/templates.js'; 2 | 3 | export default function ModalHeader({ $target, onClick }) { 4 | const $openPage = $createElement('span', '.modal-openpage-btn'); 5 | $openPage.innerHTML = ` 페이지로 열기`; 6 | 7 | const $closeModalBtn = $createElement( 8 | 'button', 9 | '.modal-close-btn', 10 | '.icon-cancel', 11 | ); 12 | 13 | this.init = () => { 14 | const { openPage, closeModal } = onClick; 15 | 16 | $target.appendChild($openPage); 17 | $target.appendChild($closeModalBtn); 18 | 19 | $openPage.addEventListener('click', e => { 20 | openPage(); 21 | }); 22 | 23 | $closeModalBtn.addEventListener('click', e => { 24 | closeModal(); 25 | }); 26 | }; 27 | 28 | this.init(); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/posts/PostsPage.js: -------------------------------------------------------------------------------- 1 | import { emit } from '../../utils/emitter.js'; 2 | import { $createElement } from '../../utils/templates.js'; 3 | import { setCurrentLi, checkDataForPlaceholder } from '../../utils/render.js'; 4 | 5 | import PageNoData from './PostsPageNoData.js'; 6 | import PageBody from './PostsPageBody.js'; 7 | 8 | export default function Page({ $target, initialState }) { 9 | const $page = $createElement('div', '.col', '.page-container'); 10 | const $pageBody = $createElement('div', '.page-body'); 11 | 12 | this.state = initialState; 13 | this.setState = nextState => { 14 | this.state = nextState; 15 | pageBody.setState(this.state); 16 | }; 17 | 18 | this.render = () => { 19 | const haveData = Object.keys(this.state.currentDocument).length > 0; 20 | 21 | if (haveData) { 22 | pageBody.render(); 23 | } else { 24 | noDataPage.render(); 25 | } 26 | }; 27 | 28 | const noDataPage = new PageNoData({ $target: $pageBody }); 29 | 30 | const pageBody = new PageBody({ 31 | $target: $pageBody, 32 | initialState, 33 | onUpdate: { 34 | updateTitle: nextDocument => { 35 | const { id } = this.state.currentDocument; 36 | const { title } = nextDocument; 37 | const $target = $('.show-page-title'); 38 | 39 | setCurrentLi({ id, title }); 40 | checkDataForPlaceholder({ $target }); 41 | emit.updateDocument({ id, nextDocument, onModal: false }); 42 | }, 43 | updateContent: nextDocument => { 44 | const { id } = this.state.currentDocument; 45 | 46 | emit.updateDocument({ id, nextDocument, onModal: false }); 47 | }, 48 | }, 49 | }); 50 | 51 | $target.appendChild($page); 52 | $page.appendChild($pageBody); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/posts/PostsPageBody.js: -------------------------------------------------------------------------------- 1 | import { $createElement, $hiddenTitleItem } from '../../utils/templates.js'; 2 | import { setPlaceholderTitle } from '../../utils/render.js'; 3 | 4 | export default function PageBody({ $target, initialState, onUpdate }) { 5 | const $pageTitle = $createElement('div', '.page-title'); 6 | const $pageContent = $createElement('div', '.page-content'); 7 | 8 | const $hiddenTitleInput = $hiddenTitleItem('hidden-page-title'); 9 | const $titleInput = $createElement('div', '.show-page-title'); 10 | $titleInput.setAttribute('contenteditable', true); 11 | 12 | const $contentInput = $createElement('textarea', '.show-page-content'); 13 | $contentInput.setAttribute('placeholder', '문서의 내용을 입력해보세요!'); 14 | 15 | this.state = initialState; 16 | this.setState = nextState => { 17 | this.state = nextState; 18 | }; 19 | 20 | this.render = () => { 21 | $target.innerHTML = ''; 22 | 23 | $pageTitle.appendChild($titleInput); 24 | $pageTitle.appendChild($hiddenTitleInput); 25 | $pageContent.appendChild($contentInput); 26 | $target.appendChild($pageTitle); 27 | $target.appendChild($pageContent); 28 | 29 | const { title, content } = this.state.currentDocument; 30 | 31 | $titleInput.textContent = title; 32 | $contentInput.value = content ? content : ''; 33 | 34 | setPlaceholderTitle({ $target: $hiddenTitleInput, title }); 35 | }; 36 | 37 | this.init = () => { 38 | $titleInput.addEventListener('keyup', e => { 39 | const title = e.target.textContent; 40 | const content = $contentInput.value; 41 | 42 | const nextDocument = { title, content }; 43 | onUpdate.updateTitle(nextDocument); 44 | }); 45 | 46 | $contentInput.addEventListener('keyup', e => { 47 | const title = $titleInput.textContent; 48 | const content = e.target.value; 49 | 50 | const nextDocument = { title, content }; 51 | onUpdate.updateContent(nextDocument); 52 | }); 53 | }; 54 | 55 | this.init(); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/posts/PostsPageNoData.js: -------------------------------------------------------------------------------- 1 | import { $createElement } from '../../utils/templates.js'; 2 | 3 | export default function PageNoData({ $target }) { 4 | const $nodataPage = $createElement('div', '.nodata-page'); 5 | const $title = $createElement('div', '.nodata-title'); 6 | const $image = $createElement('img', '.nodata-img'); 7 | $image.setAttribute('src', '/src/assets/images/index.png'); 8 | 9 | this.render = () => { 10 | $target.innerHTML = ''; 11 | $title.textContent = 'Notion에 오신 것을 환영합니다!'; 12 | 13 | $nodataPage.appendChild($title); 14 | $nodataPage.appendChild($image); 15 | $target.appendChild($nodataPage); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/sidebar/Sidebar.js: -------------------------------------------------------------------------------- 1 | import { emit } from '../../utils/emitter.js'; 2 | import { 3 | toggleList, 4 | markListItemOfId, 5 | makeNewListItemOnTree, 6 | makeNewListItemOnRoot, 7 | } from '../../utils/render.js'; 8 | import { $createElement } from '../../utils/templates.js'; 9 | 10 | import SidebarHeader from './SidebarHeader.js'; 11 | import SidebarBody from './SidebarBody.js'; 12 | import SidebarFooter from './SidebarFooter.js'; 13 | 14 | export default function Sidebar({ $target, initialState }) { 15 | const $sidebar = $createElement('div', '.col', '.sidebar-container'); 16 | const $sidebarHeader = $createElement('div', '.sidebar-header'); 17 | const $sidebarBody = $createElement('div', '.sidebar-body'); 18 | const $sidebarFooter = $createElement('div'); 19 | 20 | this.state = initialState; 21 | this.setState = nextState => { 22 | this.state = nextState; 23 | sidebarBody.setState(this.state); 24 | }; 25 | 26 | this.render = () => { 27 | sidebarBody.render(); 28 | }; 29 | 30 | new SidebarHeader({ 31 | $target: $sidebarHeader, 32 | }); 33 | 34 | const sidebarBody = new SidebarBody({ 35 | $target: $sidebarBody, 36 | initialState: this.state, 37 | onClick: { 38 | toggleList: (act, $li) => { 39 | toggleList({ act, $li }); 40 | }, 41 | readDocument: id => { 42 | markListItemOfId(id); 43 | emit.readDocument({ id }); 44 | }, 45 | deleteDocument: (id, isCurrent) => { 46 | emit.deleteDocument(id, isCurrent); 47 | }, 48 | createDocument: id => { 49 | makeNewListItemOnRoot({ needMark: true }); 50 | 51 | emit.createDocument({ id, onModal: false }); 52 | }, 53 | createDocumentOnModal: (id, $li) => { 54 | makeNewListItemOnTree({ $target: $li }); 55 | 56 | emit.showModal(); 57 | emit.createDocument({ id, onModal: true }); 58 | }, 59 | }, 60 | }); 61 | 62 | new SidebarFooter({ 63 | $target: $sidebarFooter, 64 | onClick: { 65 | createDocument: () => { 66 | const isVisible = !$('.modal-container').classList.contains('hide'); 67 | 68 | if (isVisible) { 69 | return; 70 | } 71 | 72 | makeNewListItemOnRoot({ needMark: false }); 73 | 74 | emit.showModal(); 75 | emit.createDocument({ id: null, onModal: true }); 76 | }, 77 | }, 78 | }); 79 | 80 | this.init = () => { 81 | $target.appendChild($sidebar); 82 | $sidebar.appendChild($sidebarHeader); 83 | $sidebar.appendChild($sidebarBody); 84 | $sidebar.appendChild($sidebarFooter); 85 | }; 86 | 87 | this.init(); 88 | } 89 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarBody.js: -------------------------------------------------------------------------------- 1 | import { $createElement } from '../../utils/templates.js'; 2 | 3 | import { drawNavList, markListItemOfId } from '../../utils/render.js'; 4 | import { getOpenedLiAfter } from '../../store/gettersLi.js'; 5 | 6 | export default function SidebarBody({ $target, initialState, onClick }) { 7 | const $navContainer = $createElement('div', '.nav-container'); 8 | const $navRow = $createElement('div', '.list-row'); 9 | const $navList = $createElement('div', '.nav-list'); 10 | const $ul = $createElement('ul', '.root'); 11 | const $createBtn = $createElement('div', '.create-btn'); 12 | $createBtn.innerHTML = `+ 페이지 추가`; 13 | 14 | this.state = initialState; 15 | this.setState = nextState => { 16 | this.state = nextState; 17 | }; 18 | 19 | this.render = () => { 20 | const { documents, currentDocument } = this.state; 21 | const $selected = $('p.selected'); 22 | 23 | const openedLi = getOpenedLiAfter('fetch'); 24 | 25 | $ul.innerHTML = ''; 26 | drawNavList($ul, documents, openedLi); 27 | 28 | removeClass($selected, 'selected'); 29 | markListItemOfId(currentDocument.id); 30 | }; 31 | 32 | this.init = () => { 33 | $navList.appendChild($ul); 34 | $navRow.appendChild($navList); 35 | $navContainer.appendChild($navRow); 36 | $navContainer.appendChild($createBtn); 37 | $target.appendChild($navContainer); 38 | 39 | $createBtn.addEventListener('click', e => { 40 | onClick.createDocument(null, null); 41 | }); 42 | 43 | $navList.addEventListener('mouseover', e => { 44 | const currentTarget = e.target.parentNode; 45 | const { tagName, className } = currentTarget; 46 | 47 | const $needRemoveCollection = document.querySelectorAll('.show'); 48 | const $deleteBtn = currentTarget.querySelector('.nav-delete-btn'); 49 | const $createBtn = currentTarget.querySelector('.nav-create-btn'); 50 | 51 | removeClassAll($needRemoveCollection, 'show'); 52 | 53 | if (tagName !== 'LI' && className !== 'root') { 54 | addClass($deleteBtn, 'show'); 55 | addClass($createBtn, 'show'); 56 | } 57 | }); 58 | 59 | $navList.addEventListener('mouseout', e => { 60 | const $needRemoveCollection = document.querySelectorAll('.show'); 61 | removeClassAll($needRemoveCollection, 'show'); 62 | }); 63 | 64 | $ul.addEventListener('click', e => { 65 | const { tagName, className, parentNode } = e.target; 66 | 67 | const $li = parentNode.parentNode; 68 | const { id } = $li.dataset; 69 | const { act } = e.target.dataset; 70 | 71 | if (tagName === 'LI' || className.includes('root') || className.includes('blank') ) { 72 | return; 73 | } 74 | 75 | switch (act) { 76 | case 'toggle': 77 | const isOpened = className.includes('icon-down-dir'); 78 | 79 | if (isOpened) { 80 | onClick.toggleList('hide', $li); 81 | } else { 82 | onClick.toggleList('show', $li); 83 | } 84 | break; 85 | case 'create': 86 | const onModal = !!id; 87 | 88 | if (onModal) { 89 | onClick.createDocumentOnModal(id, $li); 90 | } else { 91 | onClick.createDocument($li); 92 | } 93 | break; 94 | case 'delete': 95 | const isCurrent = Number(id) === this.state.currentDocument.id; 96 | onClick.deleteDocument(id, isCurrent); 97 | break; 98 | default: 99 | onClick.readDocument(id); 100 | break; 101 | } 102 | }); 103 | }; 104 | 105 | this.init(); 106 | } 107 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarFooter.js: -------------------------------------------------------------------------------- 1 | import { $createElement } from '../../utils/templates.js'; 2 | 3 | export default function SidebarFooter({ $target, onClick }) { 4 | const $footer = $createElement('div', '.sidebar-footer'); 5 | const $createBtn = $createElement('span', '.create-btn'); 6 | $footer.setAttribute('data-target', 'modal'); 7 | $createBtn.setAttribute('data-target', 'modal'); 8 | $createBtn.textContent = '+ 새 페이지'; 9 | 10 | $footer.appendChild($createBtn); 11 | $target.appendChild($footer); 12 | 13 | this.init = () => { 14 | $footer.addEventListener('click', e => { 15 | const { createDocument } = onClick; 16 | createDocument(); 17 | }); 18 | }; 19 | 20 | this.init(); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarHeader.js: -------------------------------------------------------------------------------- 1 | import { $createElement } from '../../utils/templates.js'; 2 | 3 | export default function SidebarHeader({ $target }) { 4 | const $headerTitlte = $createElement('div', '.header-title'); 5 | $headerTitlte.textContent = '😎 손수림의 notion'; 6 | 7 | $target.appendChild($headerTitlte); 8 | } 9 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.js'; 2 | 3 | const $target = document.querySelector('#app'); 4 | 5 | new App({ $target }); 6 | -------------------------------------------------------------------------------- /src/pages/MainPage.js: -------------------------------------------------------------------------------- 1 | import { $createElement } from '../utils/templates.js'; 2 | 3 | import Sidebar from '../components/sidebar/Sidebar.js'; 4 | import PostsPage from '../components/posts/PostsPage.js'; 5 | import Modal from '../components/modal/Modal.js'; 6 | 7 | export default function MainPage({ $target, initialState }) { 8 | const $row = $createElement('div', '.row'); 9 | 10 | this.state = initialState; 11 | this.setState = ({ nextState, needRender }) => { 12 | this.state = nextState; 13 | sideBar.setState(this.state); 14 | postsPage.setState(this.state); 15 | 16 | this.render(needRender); 17 | }; 18 | 19 | this.render = needRender => { 20 | switch (needRender) { 21 | case 'null': 22 | break; 23 | case 'sideBar': 24 | sideBar.render(); 25 | break; 26 | case 'postsPage': 27 | postsPage.render(); 28 | break; 29 | default: 30 | sideBar.render(); 31 | postsPage.render(); 32 | } 33 | }; 34 | 35 | new Modal({ $target }); 36 | 37 | const sideBar = new Sidebar({ 38 | $target: $row, 39 | initialState: this.state, 40 | }); 41 | const postsPage = new PostsPage({ 42 | $target: $row, 43 | initialState: this.state, 44 | }); 45 | 46 | $target.appendChild($row); 47 | } 48 | -------------------------------------------------------------------------------- /src/pages/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import { $createElement } from '../utils/templates.js'; 2 | 3 | export default function NotFoundPage({ $target }) { 4 | const $page = $createElement('div', '.not-found'); 5 | 6 | const $title = $createElement('h1', '.not-fount-title'); 7 | $title.textContent = '길을 잃었어요..🥺'; 8 | 9 | const $image = $createElement('img', '.not-found-img'); 10 | $image.setAttribute('src', '/src/assets/images/404.png'); 11 | 12 | this.render = () => { 13 | $target.innerHTML = ''; 14 | 15 | $page.appendChild($title); 16 | $page.appendChild($image); 17 | $target.appendChild($page); 18 | }; 19 | 20 | this.init = () => { 21 | $title.addEventListener('click', e => { 22 | window.location = window.location.origin; 23 | }); 24 | 25 | $image.addEventListener('click', e => { 26 | window.location = window.location.origin; 27 | }); 28 | }; 29 | 30 | this.init(); 31 | } 32 | -------------------------------------------------------------------------------- /src/store/gettersLi.js: -------------------------------------------------------------------------------- 1 | import { isValidOpenedLi } from '../utils/valid.js'; 2 | import { getItemFromStorage, setItemToStorage } from '../utils/storage.js'; 3 | 4 | const getOpenedLiAfter = (action, option) => { 5 | try { 6 | const newOpenedLi = gettersLi[action](option); 7 | 8 | isValidOpenedLi(newOpenedLi); 9 | 10 | gettersLi.openedLi = newOpenedLi; 11 | setItemToStorage('openedLi', newOpenedLi); 12 | 13 | return newOpenedLi; 14 | } catch (e) { 15 | alert('List에 오류가 발생하여 notion을 다시 불러옵니다!'); 16 | window.location = window.origin; 17 | } 18 | }; 19 | 20 | const gettersLi = { 21 | openedLi: [], 22 | fetch: () => { 23 | gettersLi.openedLi = getItemFromStorage('openedLi', []) || []; 24 | return gettersLi.openedLi; 25 | }, 26 | add: ({ id }) => { 27 | const newOpenedLi = [...gettersLi.openedLi]; 28 | 29 | if (!newOpenedLi.includes(id)) { 30 | newOpenedLi.push(id); 31 | } 32 | 33 | return newOpenedLi; 34 | }, 35 | delete: ({ id }) => { 36 | const newOpenedLi = [...gettersLi.openedLi]; 37 | 38 | if (newOpenedLi.includes) { 39 | newOpenedLi.splice(newOpenedLi.indexOf(id), 1); 40 | } 41 | 42 | return newOpenedLi; 43 | }, 44 | }; 45 | 46 | export { getOpenedLiAfter }; 47 | -------------------------------------------------------------------------------- /src/store/gettersState.js: -------------------------------------------------------------------------------- 1 | import { isValidState } from '../utils/valid.js'; 2 | import { getItemFromStorage, setItemToStorage } from '../utils/storage.js'; 3 | 4 | import { 5 | getDocuments, 6 | createDocument, 7 | updateDocument, 8 | deleteDocument, 9 | } from '../api/notion.js'; 10 | 11 | const getStateAfter = async (action, option) => { 12 | try { 13 | const newState = await getters[action](option); 14 | 15 | isValidState(newState); 16 | 17 | if (newState && !action.includes('Modal')) { 18 | setItemToStorage('notionState', newState); 19 | } 20 | 21 | return newState; 22 | } catch (e) { 23 | alert('state에 오류가 발생하여 notion을 다시 불러옵니다!'); 24 | window.location = window.origin; 25 | } 26 | }; 27 | 28 | const getters = { 29 | fetch: async () => { 30 | const nextState = {}; 31 | 32 | const { pathname } = window.location; 33 | const [, , id] = pathname.split('/'); 34 | let postId = id; 35 | 36 | nextState.documents = await getDocuments(); 37 | 38 | if (postId) { 39 | nextState.currentDocument = await getDocuments(postId); 40 | } else { 41 | nextState.currentDocument = {}; 42 | } 43 | 44 | return nextState; 45 | }, 46 | create: async id => { 47 | const currentDocument = await createDocument({ 48 | title: '', 49 | parent: id, 50 | }); 51 | const documents = await getDocuments(); 52 | 53 | return { documents, currentDocument }; 54 | }, 55 | createOnModal: async id => { 56 | const modalDocument = await createDocument({ 57 | title: '', 58 | parent: id, 59 | }); 60 | 61 | const documents = await getDocuments(); 62 | const { currentDocument } = getItemFromStorage('notionState'); 63 | 64 | setItemToStorage('notionState', { documents, currentDocument }); 65 | return { documents, currentDocument, modalDocument }; 66 | }, 67 | read: async id => { 68 | const { documents } = getItemFromStorage('notionState'); 69 | const currentDocument = await getDocuments(id); 70 | 71 | return { documents, currentDocument }; 72 | }, 73 | update: async ({ id, nextDocument }) => { 74 | const updatedDocument = await updateDocument(id, nextDocument); 75 | const documents = await getDocuments(); 76 | 77 | return { documents, currentDocument: updatedDocument }; 78 | }, 79 | updateOnModal: async ({ id, nextDocument }) => { 80 | await updateDocument(id, nextDocument); 81 | const documents = await getDocuments(); 82 | const { currentDocument } = getItemFromStorage('notionState'); 83 | 84 | setItemToStorage('notionState', { documents, currentDocument }); 85 | return { documents, currentDocument }; 86 | }, 87 | delete: async id => { 88 | await deleteDocument(id); 89 | 90 | const documents = await getDocuments(); 91 | const { currentDocument } = getItemFromStorage('notionState'); 92 | 93 | return { documents, currentDocument }; 94 | }, 95 | deleteCurrent: async id => { 96 | await deleteDocument(id); 97 | 98 | let postId; 99 | const documents = await getDocuments(); 100 | 101 | if (documents.length) { 102 | postId = documents[0].id; 103 | } 104 | 105 | const currentDocument = await getDocuments(postId); 106 | return { documents, currentDocument }; 107 | }, 108 | }; 109 | 110 | export { getStateAfter }; 111 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { on, emit } from '../utils/emitter.js'; 2 | 3 | import { isNeedProtect } from '../utils/valid.js'; 4 | import { getStateAfter } from './gettersState.js'; 5 | import { setElementAfter } from './settersElement.js'; 6 | 7 | export default function Store() { 8 | let updateTimer = null; 9 | 10 | const commit = (mutation, options) => { 11 | this.mutations[mutation](options); 12 | }; 13 | 14 | const dispatch = (action, options) => { 15 | this.actions[action](options); 16 | }; 17 | 18 | this.mutations = { 19 | SET_STATE: ({ nextState, needRender }) => { 20 | emit.updateState(nextState, needRender); 21 | }, 22 | }; 23 | 24 | this.actions = { 25 | createDocument: async ({ id }) => { 26 | const nextState = await getStateAfter('create', id); 27 | const newPostId = nextState.currentDocument.id; 28 | 29 | commit('SET_STATE', { nextState, needRender: 'postsPage' }); 30 | setElementAfter('create', { nextState }); 31 | history.pushState(null, null, `/documents/${newPostId}`); 32 | }, 33 | createDocumentOnModal: async ({ id }) => { 34 | const { documents, currentDocument, modalDocument } = await getStateAfter( 35 | 'createOnModal', 36 | id, 37 | ); 38 | 39 | commit('SET_STATE', { 40 | nextState: { documents, currentDocument }, 41 | needRender: 'null', 42 | }); 43 | setElementAfter('createOnModal', { modalDocument }); 44 | }, 45 | readDocument: async ({ id }) => { 46 | const nextState = await getStateAfter('read', id); 47 | 48 | commit('SET_STATE', { nextState, needRender: 'postsPage' }); 49 | history.pushState(null, null, `/documents/${id}`); 50 | }, 51 | updateDocument: async ({ id, nextDocument, onModal }) => { 52 | if (updateTimer) { 53 | clearTimeout(updateTimer); 54 | } 55 | 56 | updateTimer = setTimeout(async () => { 57 | const action = onModal ? 'updateOnModal' : 'update'; 58 | 59 | const nextState = await getStateAfter(action, { id, nextDocument }); 60 | commit('SET_STATE', { nextState, needRender: 'null' }); 61 | }, 200); 62 | }, 63 | deleteDocument: async ({ id }) => { 64 | if (confirm('문서를 삭제하시겠습니까?')) { 65 | const nextState = await getStateAfter('delete', id); 66 | 67 | setElementAfter('delete', { id, nextState }); 68 | commit('SET_STATE', { nextState, needRender: 'sideBar' }); 69 | } 70 | }, 71 | deleteCurrentDocument: async ({ id }) => { 72 | if (confirm('문서를 삭제하시겠습니까?')) { 73 | const nextState = await getStateAfter('deleteCurrent', id); 74 | 75 | setElementAfter('deleteCurrent', { id, nextState }); 76 | commit('SET_STATE', { nextState, needRender: 'all' }); 77 | } 78 | }, 79 | deleteEmptyDocument: async ({ id }) => { 80 | const nextState = await getStateAfter('delete', id); 81 | 82 | setElementAfter('delete', { id, nextState: nextState.currentDocument }); 83 | commit('SET_STATE', { nextState, needRender: 'sideBar' }); 84 | }, 85 | }; 86 | 87 | this.init = () => { 88 | on.createDocument(({ id, onModal }) => { 89 | if (onModal) { 90 | dispatch('createDocumentOnModal', { id }); 91 | } else { 92 | dispatch('createDocument', { id }); 93 | } 94 | }); 95 | on.readDocument(({ id }) => dispatch('readDocument', { id })); 96 | on.updateDocument(({ id, nextDocument, onModal }) => 97 | dispatch('updateDocument', { id, nextDocument, onModal }), 98 | ); 99 | on.deleteDocument((id, isCurrent) => { 100 | if (!isNeedProtect(id)) { 101 | return; 102 | } 103 | 104 | if (isCurrent) { 105 | dispatch('deleteCurrentDocument', { id }); 106 | } else { 107 | dispatch('deleteDocument', { id }); 108 | } 109 | }); 110 | on.deleteEmptyDocument(id => dispatch('deleteEmptyDocument', { id })); 111 | }; 112 | 113 | this.init(); 114 | } 115 | -------------------------------------------------------------------------------- /src/store/settersElement.js: -------------------------------------------------------------------------------- 1 | import { emit } from '../utils/emitter.js'; 2 | 3 | import { 4 | closeChildList, 5 | markListItemOfId, 6 | setListItemToDataId, 7 | } from '../utils/render.js'; 8 | 9 | const setElementAfter = (action, options) => { 10 | setters[action](options); 11 | }; 12 | 13 | const setters = { 14 | create: ({ nextState }) => { 15 | const newPostId = nextState.currentDocument.id; 16 | 17 | setListItemToDataId(newPostId); 18 | }, 19 | createOnModal: ({ modalDocument }) => { 20 | const newPostId = modalDocument.id; 21 | 22 | setListItemToDataId(newPostId); 23 | emit.updateModal(modalDocument); 24 | }, 25 | delete: ({ id, nextState }) => { 26 | closeChildList(id); 27 | markListItemOfId(nextState.id); 28 | }, 29 | deleteCurrent: ({ id, nextState }) => { 30 | const nextId = nextState.currentDocument.id; 31 | let url = '/'; 32 | 33 | if (nextId) { 34 | url = `/documents/${nextId}`; 35 | 36 | const $needRemoveSelected = $(`li[data-id="${id}"] .selected`); 37 | closeChildList(id); 38 | removeClass($needRemoveSelected, 'selected'); 39 | markListItemOfId(nextId); 40 | } 41 | 42 | history.replaceState(null, null, url); 43 | }, 44 | }; 45 | 46 | export { setElementAfter }; 47 | -------------------------------------------------------------------------------- /src/utils/emitter.js: -------------------------------------------------------------------------------- 1 | const UPDATE_STATE = 'update:state'; 2 | const SHOW_MODAL = 'show:modal'; 3 | const UPDATE_MODAL = 'update:modal'; 4 | const TOGGLE_LIST = 'toggle:list'; 5 | const CREATE_DOCUMENT = 'create:document'; 6 | const READ_DOCUMENT = 'read:document'; 7 | const UPDATE_DOCUMENT = 'update:document'; 8 | const DELETE_DOCUMENT = 'delete:document'; 9 | const DELETE_DOCUMENT_EMPTY = 'delete:emptyDocument'; 10 | 11 | const on = { 12 | initRouter(onUpdate) { 13 | window.addEventListener('popstate', e => { 14 | onUpdate(); 15 | }); 16 | }, 17 | initStore(onUpdate) { 18 | window.addEventListener(UPDATE_STATE, e => { 19 | const { nextState, needRender } = e.detail; 20 | onUpdate(nextState, needRender); 21 | }); 22 | }, 23 | showModal(onShow) { 24 | window.addEventListener(SHOW_MODAL, e => { 25 | onShow(); 26 | }); 27 | }, 28 | updateModal(onUpdate) { 29 | window.addEventListener(UPDATE_MODAL, e => { 30 | const { nextState } = e.detail; 31 | onUpdate(nextState); 32 | }); 33 | }, 34 | toggleList(onToggle) { 35 | window.addEventListener(TOGGLE_LIST, e => { 36 | const { act, $li } = e.detail; 37 | onToggle({ act, $li }); 38 | }); 39 | }, 40 | createDocument(onCreate) { 41 | window.addEventListener(CREATE_DOCUMENT, e => { 42 | const { id, $target, needMark, onModal } = e.detail; 43 | onCreate({ id, $target, needMark, onModal }); 44 | }); 45 | }, 46 | readDocument(onRead) { 47 | window.addEventListener(READ_DOCUMENT, e => { 48 | const { id } = e.detail; 49 | onRead(id); 50 | }); 51 | }, 52 | updateDocument(onUpdate) { 53 | window.addEventListener(UPDATE_DOCUMENT, e => { 54 | const { id, nextDocument, onModal } = e.detail; 55 | 56 | if (id && nextDocument) { 57 | onUpdate({ id, nextDocument, onModal }); 58 | } 59 | }); 60 | }, 61 | deleteDocument(onDelete) { 62 | window.addEventListener(DELETE_DOCUMENT, e => { 63 | const { id, isCurrent } = e.detail; 64 | 65 | if (id) { 66 | onDelete(id, isCurrent); 67 | } 68 | }); 69 | }, 70 | deleteEmptyDocument(onDelete) { 71 | window.addEventListener(DELETE_DOCUMENT_EMPTY, e => { 72 | const { id } = e.detail; 73 | 74 | if (id) { 75 | onDelete(id); 76 | } 77 | }); 78 | }, 79 | }; 80 | const emit = { 81 | updateState(nextState, needRender) { 82 | window.dispatchEvent( 83 | new CustomEvent(UPDATE_STATE, { 84 | detail: { 85 | nextState, 86 | needRender, 87 | }, 88 | }), 89 | ); 90 | }, 91 | showModal() { 92 | window.dispatchEvent(new CustomEvent(SHOW_MODAL)); 93 | }, 94 | updateModal(nextState) { 95 | window.dispatchEvent( 96 | new CustomEvent(UPDATE_MODAL, { 97 | detail: { 98 | nextState, 99 | }, 100 | }), 101 | ); 102 | }, 103 | toggleList({ act, $li }) { 104 | window.dispatchEvent( 105 | new CustomEvent(TOGGLE_LIST, { 106 | detail: { act, $li }, 107 | }), 108 | ); 109 | }, 110 | createDocument({ id, $target, needMark, onModal }) { 111 | window.dispatchEvent( 112 | new CustomEvent(CREATE_DOCUMENT, { 113 | detail: { 114 | id, 115 | $target, 116 | needMark, 117 | onModal, 118 | }, 119 | }), 120 | ); 121 | }, 122 | readDocument(id) { 123 | window.dispatchEvent( 124 | new CustomEvent(READ_DOCUMENT, { 125 | detail: { id }, 126 | }), 127 | ); 128 | }, 129 | updateDocument({ id, nextDocument, onModal }) { 130 | window.dispatchEvent( 131 | new CustomEvent(UPDATE_DOCUMENT, { 132 | detail: { id, nextDocument, onModal }, 133 | }), 134 | ); 135 | }, 136 | deleteDocument(id, isCurrent) { 137 | window.dispatchEvent( 138 | new CustomEvent(DELETE_DOCUMENT, { 139 | detail: { id, isCurrent }, 140 | }), 141 | ); 142 | }, 143 | deleteEmptyDocument(id) { 144 | window.dispatchEvent( 145 | new CustomEvent(DELETE_DOCUMENT_EMPTY, { 146 | detail: { id }, 147 | }), 148 | ); 149 | }, 150 | }; 151 | 152 | export { on, emit }; 153 | -------------------------------------------------------------------------------- /src/utils/render.js: -------------------------------------------------------------------------------- 1 | import { getOpenedLiAfter } from '../store/gettersLi.js'; 2 | 3 | import { 4 | $createElement, 5 | $blankItem, 6 | $listItem, 7 | $treeItem, 8 | $newPostListItem, 9 | } from './templates.js'; 10 | 11 | const drawNavList = (target, childDocuments, openedLi) => { 12 | childDocuments.forEach(({ id, title, documents }) => { 13 | const $li = $listItem(); 14 | const haveChild = documents.length > 0; 15 | 16 | if (haveChild) { 17 | const $tree = $treeItem(); 18 | 19 | drawNavList($tree, documents, openedLi); 20 | addClass($li, 'nav-header', 'tree-toggler'); 21 | 22 | $li.appendChild($tree); 23 | } else { 24 | const $blank = $blankItem(); 25 | $li.appendChild($blank); 26 | } 27 | const $filledList = fillListItem($li, { id, title, openedLi }); 28 | target.appendChild($filledList); 29 | }); 30 | }; 31 | 32 | const makeNewListItemOnTree = ({ $target }) => { 33 | const $newPostLi = $newPostListItem(); 34 | const $blank = $target.querySelector(':scope > .blank'); 35 | 36 | if ($blank) { 37 | $blank.remove(); 38 | 39 | const $tree = $createElement('ul', '.tree'); 40 | $tree.appendChild($newPostLi); 41 | 42 | $target.appendChild($tree); 43 | addClass($target, 'nav-header', 'tree-toggler'); 44 | } else { 45 | const $tree = $target.querySelector(':scope > .tree'); 46 | $tree.appendChild($newPostLi); 47 | } 48 | 49 | toggleList({ act: 'show', $li: $target }); 50 | }; 51 | 52 | const makeNewListItemOnRoot = ({ needMark }) => { 53 | const $newPostLi = $newPostListItem(); 54 | 55 | if (needMark) { 56 | markListItemofLi($newPostLi); 57 | } 58 | 59 | $('.root').appendChild($newPostLi); 60 | }; 61 | 62 | const toggleList = ({ act, $li }) => { 63 | const { id } = $li.dataset; 64 | 65 | if (act === 'show') { 66 | getOpenedLiAfter('add', { id }); 67 | 68 | const $hidden = $li.querySelector(':scope > .hide'); 69 | const $toggleBtn = $li.querySelector(':scope > p .icon-play'); 70 | 71 | removeClass($hidden, 'hide'); 72 | replaceClass($toggleBtn, 'icon-play', 'icon-down-dir'); 73 | return; 74 | } 75 | 76 | if (act === 'hide') { 77 | getOpenedLiAfter('delete', { id }); 78 | 79 | const $needHide = $li.querySelector('.tree') || $li.querySelector('.blank'); 80 | const $toggleBtn = $li.querySelector('.icon-down-dir'); 81 | 82 | addClass($needHide, 'hide'); 83 | replaceClass($toggleBtn, 'icon-down-dir', 'icon-play'); 84 | } 85 | }; 86 | 87 | const fillListItem = ($li, { id, title, openedLi }) => { 88 | const isOpenedLi = openedLi?.includes(`${id}`); 89 | 90 | const $nearHide = $li.querySelector('.hide'); 91 | const $toggleBtn = $li.querySelector('.nav-toggler-btn'); 92 | const $pageTitle = $li.querySelector('.nav-page-title'); 93 | 94 | if (isOpenedLi) { 95 | removeClass($nearHide, 'hide'); 96 | replaceClass($toggleBtn, 'icon-play', 'icon-down-dir'); 97 | } 98 | 99 | $pageTitle.textContent = title ? title : '제목 없음'; 100 | $li.setAttribute('data-id', id); 101 | return $li; 102 | }; 103 | 104 | const markListItemOfId = id => { 105 | const $currentSelect = $('.nav-item.selected'); 106 | const $needMark = $(`li[data-id="${id}"] p`); 107 | 108 | removeClass($currentSelect, 'selected'); 109 | addClass($needMark, 'selected'); 110 | }; 111 | 112 | const markListItemofLi = $li => { 113 | const $currentSelect = $('.nav-item.selected'); 114 | const $needMark = $li.querySelector('.nav-item'); 115 | 116 | removeClass($currentSelect, 'selected'); 117 | addClass($needMark, 'selected'); 118 | }; 119 | 120 | const setListItemToDataId = newPostId => { 121 | const $newPostLi = $('li[data-id="new"]'); 122 | $newPostLi.setAttribute('data-id', newPostId); 123 | }; 124 | 125 | const closeChildList = id => { 126 | const $childTreeCollection = $(`li[data-id="${id}"]`).querySelectorAll( 127 | '.tree-toggler .tree:not(.hide)', 128 | ); 129 | const $childBlankCollection = $(`li[data-id="${id}"]`).querySelectorAll( 130 | 'li:not(.tree-toggler) .blank:not(.hide)', 131 | ); 132 | 133 | $childTreeCollection?.forEach(tree => { 134 | const id = tree.parentNode.dataset.id; 135 | getOpenedLiAfter('delete', { id }); 136 | }); 137 | 138 | $childBlankCollection?.forEach(blank => { 139 | const id = blank.parentNode.dataset.id; 140 | getOpenedLiAfter('delete', { id }); 141 | }); 142 | }; 143 | 144 | const setCurrentLi = ({ id, title }) => { 145 | const currentLi = $(`li[data-id="${id}"] .nav-page-title`); 146 | currentLi.textContent = title ? title : '제목 없음'; 147 | }; 148 | 149 | const checkDataForPlaceholder = ({ $target }) => { 150 | const noData = $target.textContent === ''; 151 | const $hiddenInput = $target.nextSibling; 152 | 153 | if (noData) { 154 | removeClass($hiddenInput, 'hide'); 155 | } else { 156 | addClass($hiddenInput, 'hide'); 157 | } 158 | }; 159 | 160 | const setPlaceholderTitle = ({ $target, title }) => { 161 | if (!title) { 162 | removeClass($target, 'hide'); 163 | } else { 164 | addClass($target, 'hide'); 165 | } 166 | }; 167 | 168 | export { 169 | drawNavList, 170 | makeNewListItemOnTree, 171 | makeNewListItemOnRoot, 172 | fillListItem, 173 | setListItemToDataId, 174 | markListItemOfId, 175 | markListItemofLi, 176 | toggleList, 177 | closeChildList, 178 | setCurrentLi, 179 | setPlaceholderTitle, 180 | checkDataForPlaceholder, 181 | }; 182 | -------------------------------------------------------------------------------- /src/utils/selector.js: -------------------------------------------------------------------------------- 1 | function $($element) { 2 | if (!$element) { 3 | return; 4 | } 5 | 6 | return document.querySelector($element); 7 | } 8 | 9 | function addClass($element, ...classNames) { 10 | if (!$element) { 11 | return; 12 | } 13 | 14 | classNames.forEach(className => $element?.classList.add(className)); 15 | } 16 | 17 | function addClassAll($NodeList, className) { 18 | $NodeList.forEach($element => { 19 | $element?.classList.add(className); 20 | }); 21 | } 22 | 23 | function removeClass($element, ...classNames) { 24 | if (!$element) { 25 | return; 26 | } 27 | 28 | classNames.forEach(className => $element?.classList.remove(className)); 29 | } 30 | 31 | function removeClassAll($NodeList, className) { 32 | $NodeList.forEach($element => { 33 | $element?.classList.remove(className); 34 | }); 35 | } 36 | 37 | function replaceClass($element, currentClassName, replaceClassName) { 38 | if (!$element) { 39 | return; 40 | } 41 | 42 | $element.classList.replace(currentClassName, replaceClassName); 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | const STORAGE = window.sessionStorage; 2 | 3 | const getItemFromStorage = (key, defaultValue) => { 4 | try { 5 | const parsedItem = JSON.parse(STORAGE.getItem(key)); 6 | 7 | if (typeof parsedItem !== 'object') { 8 | throw new Error('올바른 데이터 형식이 아닙니다!'); 9 | } 10 | 11 | return parsedItem; 12 | } catch (e) { 13 | STORAGE.removeItem(key); 14 | console.log(e.message); 15 | return defaultValue; 16 | } 17 | }; 18 | 19 | const setItemToStorage = (key, item) => { 20 | STORAGE.setItem(key, JSON.stringify(item)); 21 | }; 22 | 23 | export { getItemFromStorage, setItemToStorage }; 24 | -------------------------------------------------------------------------------- /src/utils/templates.js: -------------------------------------------------------------------------------- 1 | import { fillListItem } from './render.js'; 2 | 3 | function $createElement(element, ...option) { 4 | const $element = document.createElement(element); 5 | 6 | if (option.length) { 7 | option.forEach(option => { 8 | const optionName = option.substring(1); 9 | 10 | switch (option[0]) { 11 | case '#': 12 | $element.setAttribute('id', optionName); 13 | break; 14 | default: 15 | $element.classList.add(optionName); 16 | } 17 | }); 18 | } 19 | 20 | return $element; 21 | } 22 | 23 | const $hiddenTitleItem = className => { 24 | const $hiddenTitleInput = $createElement('div', '.hide'); 25 | 26 | $hiddenTitleInput.setAttribute('contenteditable', true); 27 | $hiddenTitleInput.textContent = '제목 없음'; 28 | addClass($hiddenTitleInput, className); 29 | 30 | return $hiddenTitleInput; 31 | }; 32 | 33 | const $blankItem = () => { 34 | const $blank = $createElement('p', '.blank', '.hide'); 35 | 36 | $blank.setAttribute('datat-id', 'blank'); 37 | $blank.classList.add('hide'); 38 | $blank.textContent = '하위 페이지가 없습니다.'; 39 | 40 | return $blank; 41 | }; 42 | 43 | const $treeItem = () => { 44 | const $tree = $createElement('ul', '.tree', '.hide'); 45 | return $tree; 46 | }; 47 | 48 | const $listItem = () => { 49 | const $li = $createElement('li'); 50 | 51 | $li.innerHTML = ` 52 | `; 59 | 60 | return $li; 61 | }; 62 | 63 | const $newPostListItem = () => { 64 | const $newLi = $listItem(); 65 | const $filledLi = fillListItem($newLi, { 66 | id: 'new', 67 | title: '제목 없음', 68 | isOpened: null, 69 | }); 70 | const $blank = $blankItem(); 71 | 72 | $filledLi.appendChild($blank); 73 | return $filledLi; 74 | }; 75 | 76 | export { 77 | $createElement, 78 | $hiddenTitleItem, 79 | $treeItem, 80 | $listItem, 81 | $blankItem, 82 | $newPostListItem, 83 | }; 84 | -------------------------------------------------------------------------------- /src/utils/valid.js: -------------------------------------------------------------------------------- 1 | const isValidState = state => { 2 | const DEFAULT_STATE = { 3 | documents: {}, 4 | currentDocument: {}, 5 | }; 6 | 7 | let validResult = true; 8 | 9 | if (state && typeof state === 'object') { 10 | for (let key in DEFAULT_STATE) { 11 | validResult = state.hasOwnProperty(key); 12 | } 13 | } 14 | 15 | if (!validResult) { 16 | throw new Error('올바른 데이터 형식이 아닙니다!'); 17 | } 18 | }; 19 | 20 | const isValidOpenedLi = openedLi => { 21 | if (!openedLi || !Array.isArray(openedLi)) { 22 | throw new Error('올바른 데이터 형식이 아닙니다!'); 23 | } 24 | }; 25 | 26 | const isNeedProtect = id => { 27 | const PROTECT_DOCUMENT = ['16238', '16564', '17594', '17596', '18040', '18041', '18053', '18486', '18580', '19881', '18625', '18581', '19938', '19020', '19034', '19035', '19891']; 28 | let result = true; 29 | 30 | if (PROTECT_DOCUMENT.includes(id)) { 31 | alert('문서 지우지 말아주세요....🥺') 32 | result = false; 33 | } 34 | 35 | return result; 36 | } 37 | 38 | export { isValidState, isValidOpenedLi, isNeedProtect }; -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }] 3 | } 4 | --------------------------------------------------------------------------------