├── .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 | 
7 |
8 |
12 |
13 | ## 📌 프로젝트 설명
14 |
15 | ### 앱 구조
16 |
17 | 
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 |
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 |
53 |
54 |
55 |
56 |
57 |
58 |
`;
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 |
--------------------------------------------------------------------------------