Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env.js
15 changes: 15 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="ko">

<head>
<meta charset="UTF-8">
<title>Fry's Notion</title>
<link rel="stylesheet" href="/style.css" type="text/css" />
</head>

<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>

</html>
30 changes: 30 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import MainPage from "./pages/MainPage.js";
import NotFound from "./pages/NotFound.js";
import Router from "./Router.js";

import { routes } from "./constants/routes.js";

export default function App({ $target }) {
this.$target = $target;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ask) 코드를 읽어보니 모두 this.$target = $target 으로 초기 설정을 하셨는데 이유가 뭔가요????? 궁금합니다.. 뭔가 규란님이라면 이유가 있어서 하셨을 것 같아서요 !!

this.state = { currentPage: null };

this.init = () => {
this.currentPage = new MainPage({ $target: this.$target });
new Router({ $target: this.$target, onRoute: this.route.bind(this) });
};

this.route = () => {
const findMatchedRoute = () => routes.find((route) => route.path.test(location.pathname));
const Page = findMatchedRoute()?.element || NotFound;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 잘 못 찾는 것 같습니다! 혹시 Page가 어디서 어떻게 사용되는지 알 수 있을까요? element를 routes에서 찾고 나서 렌더링하려면 new Page()처럼 실행이 되어야 할 것 같은데 보이지 않아서요!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ask) 제가 아직 자바스크립트에 능통하지 못합니다.. findMatchedRoute()?.element에서 바로 뒤에 .연산자(?)가 붙으면 객체가 반환되는 건가요?

const [, , documentId] = location.pathname.split("/");

if (documentId) {
this.currentPage.setState({ documentId });
}

console.log(window.location.pathname);
console.log(/^\/documents\//.test(location.pathname));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그 지우기~~!!!~!

};

this.init();
}
29 changes: 29 additions & 0 deletions src/Router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import NotFound from "./pages/NotFound.js";

import { HISTORY_CHANGE_EVENT_NAME, routes } from "./constants/routes.js";

export default function Router({ $target, onRoute }) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 Router 이렇게 생성자 함수를 통해 정의하신 이유가 따로 있나요??😀

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 궁금해요! 👀

this.$target = $target;
this.state = { currentPage: null };

this.init = () => {
window.addEventListener(HISTORY_CHANGE_EVENT_NAME, ({ detail }) => {
const { to, isReplace } = detail;

if (isReplace || to === location.pathname) {
history.replaceState(null, "", to);
} else {
history.pushState(null, "", to);
}

onRoute();
});

window.addEventListener("popstate", () => {
onRoute();
});
};

this.init();
onRoute();
}
24 changes: 24 additions & 0 deletions src/components/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default function Component({ $target, initialState }) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) 현재 사용되지 않는 파일 같네요! 아마 클래스로 짜시다가 중간에 함수형으로 바꾸셨다고 했는데 그 과정에서 남기신게 아닐까.. 추측해봅니다!

this.$target = $target;
this.state = { ...initialState };

this.init = () => {
this.render();
};

this.setState = () => {
this.render();
};

this.render = () => {
this.$target.innerHTML = `
`;
this.mounted();
};

this.mounted = () => {};

this.setEvent = () => {};

this.init();
}
106 changes: 106 additions & 0 deletions src/components/document/Document.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import DocumentHeader from "./DocumentHeader.js";
import DocumentContent from "./DocumentContent.js";

import API from "../../utils/api.js";
import { USER } from "../../config.js";
import { debounce } from "../../utils/debounce.js";
import { getItemFromStorage, setItemToStorage } from "../../utils/storage.js";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import 구문도 잘 나누셔서 마음이 편안해집니다. ㅎㅎㅎ


export default function Document({
$target,
initialState = {
documentId: null,
},
}) {
const handleDocumentEdit = debounce(async (text, type = "title") => {
const storedItem = getItemFromStorage("notion", { currentDocument: {} });

storedItem.currentDocument = {
...storedItem.currentDocument,
[type]: text,
tempSavedAt: new Date(),
};
setItemToStorage("notion", { ...storedItem });
await updateDocument(this.state.documentId, storedItem.currentDocument);
}, 300);

const fetchDocument = async (documentId) => {
const response = await API.getDocuments(documentId);
Comment on lines +15 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API를 객체로 선언하시다니...! 이렇게 작성하면 실수를 줄일 수 있겠네요!😲


if (!response) {
return [{ title: "", content: "" }, {}];
}
return [{ title: response.title, content: response.content }, response];
};

const updateDocument = async (documentId, newDocument) => {
const { title, content } = newDocument;
await API.updateDocument(documentId, { title, content });
};

const renderNoDocument = ($container) => {
$container.innerHTML = `<h1>🎉 Welcome to ${USER.NAME}'s Notion! 🎉</h1>`;
};

const renderNewDocument = ($header, $body) => {
new DocumentHeader({ $target: $header, onEdit: handleDocumentEdit.bind(this) });
new DocumentContent({ $target: $body, onEdit: handleDocumentEdit.bind(this) });
};

const renderDocumentById = async ($header, $body) => {
const { documentId } = this.state;
const [{ title, content }, response] = await fetchDocument(documentId);

setItemToStorage("notion", { currentDocument: response });

new DocumentHeader({
$target: $header,
initialState: { title },
onEdit: handleDocumentEdit.bind(this),
});
new DocumentContent({
$target: $body,
initialState: { content },
onEdit: handleDocumentEdit.bind(this),
});
};

this.$target = $target;
this.state = initialState;

this.init = () => {
this.render();
};

this.render = () => {
this.$target.innerHTML = `
<div id="document">
<div id="document-header"></div>
<div id="document-body"></div>
</div>
`;

this.mounted();
};

this.mounted = async () => {
const $container = this.$target.querySelector("#document");
const $header = this.$target.querySelector("#document-header");
const $body = this.$target.querySelector("#document-body");

const { documentId } = this.state;

if (documentId === "new") {
renderNewDocument($header, $body);
} else if (!!documentId) {
console.log(documentId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

꺄악! console.log!

renderDocumentById($header, $body);
} else {
renderNoDocument($container);
}
};

this.setEvent = () => {};

this.init();
}
39 changes: 39 additions & 0 deletions src/components/document/DocumentContent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { addEvent } from "../../utils/event.js";

export default function DocumentContent({
$target,
initialState = {
content: "",
},
onEdit,
}) {
this.$target = $target;
this.state = initialState;

this.init = () => {
this.setEvent();
this.render();
};

this.setState = (newState) => {
this.state = { ...this.state, ...newState };
this.render();
};

this.render = () => {
const { content } = this.state;

this.$target.innerHTML = `
<div id="document-editor" name="content" contenteditable="true" placeholder="Type for creating new document">${
content ?? ""
}</div>
`;
};

this.setEvent = () => {
addEvent(this.$target, "keyup", "[name=content]", (event) => {
onEdit(event.target.innerText, "content");
});
};
Comment on lines +29 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이벤트 리스너를 따로 함수로 만드신 건가요?! 이렇게 코드를 작성할 수도 있군요... 😮

this.init();
}
39 changes: 39 additions & 0 deletions src/components/document/DocumentHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { DEFAULT } from "../../config.js";
import { addEvent } from "../../utils/event.js";

export default function DocumentHeader({
$target,
initialState = {
title: "",
},
onEdit,
}) {
this.$target = $target;
this.state = initialState;

this.init = () => {
this.setEvent();
this.render();
};

this.setState = () => {
this.render();
};

this.render = () => {
const { title } = this.state;

this.$target.innerHTML = `
<div class="title" name="title" placeholder=${DEFAULT.DOCUMENT_NAME} contenteditable="true">${
title === DEFAULT.DOCUMENT_NAME ? "" : title
}</div>`;
};

this.setEvent = () => {
addEvent(this.$target, "keyup", "[name=title]", (event) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P5 : 저의 경우에는 event type을 keyup으로 해두면 화살표 키나 shift, caps lock 등 입력 값이 변하지 않았음에도 이벤트가 발생해서 유효하지 않은 api가 날라가더라구요. 그래서 input으로 바꿔서 입력값이 변할 때에만 api가 날라갈 수 있게 했었습니다.

그리고 이전에 React에서 겪은 적이 있는데, event type을 keyup으로 해두고 한글을 입력하면 이벤트가 중복해서 2번 발생하는 현상이 있었습니다. 실제로 검색해보니 javascript 자체의 문제더라구요. 한글 관련 에러여서 공식 자료도 찾기 어려웠었습니다. 이런 현상을 피하기 위해서 다른 event type을 고려해보시는 것도 좋을 것 같습니다!

p.s 과제에서 확인해보니, 여기선 keyup으로 해도 이벤트가 중복 발생하진 않네요!

reference : https://blog.naver.com/PostView.naver?blogId=bbak0105&logNo=222371386648&parentCategoryNo=&categoryNo=44&viewDate=&isShowPopularPosts=true&from=search

onEdit(event.target.innerText, "title");
});
};

this.init();
}
68 changes: 68 additions & 0 deletions src/components/sidebar/Details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import DocumentItem from "./DocumentItem.js";

import { $documentsList } from "../../utils/templates.js";

export default function Details({
$target,
initialState = {
id: null,
title: "",
documents: [],
},
onAddButtonClick,
}) {
this.$target = $target;
this.state = initialState;

this.init = () => {
this.render();
};

this.setState = (nextState) => {
this.state = { ...this.state, ...nextState };
this.render();
};

this.render = () => {
const { documents } = this.state;

this.$target.innerHTML = `
<details id="document-details">
<summary></summary>
<ul>${$documentsList(documents)}</ul>
</details>
`;

this.mounted();
};

this.mounted = () => {
const $rootDocument = this.$target.querySelector("summary");
const { id, title, documents } = this.state;

new DocumentItem({
$target: $rootDocument,
initialState: {
id,
title,
},
onAddButtonClick,
});

documents.forEach(({ id, title, documents }) => {
const $li = this.$target.querySelector(`[data-document-id="${id}"]`);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) SidebarBody에서는 findDocumentElement()로 따로 메서드를 만들었는데, 여기에도 적용해 볼 수 있을까요? 일종의 통일성을 위해서요


new Details({
$target: $li,
initialState: {
title,
id,
documents,
},
onAddButtonClick,
});
});
};

this.init();
}
32 changes: 32 additions & 0 deletions src/components/sidebar/DocumentItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export default function DocumentItem({ $target, initialState }) {
this.$target = $target;
this.state = initialState;

this.init = () => {
this.render();
};

this.render = () => {
const { title, id } = this.state;

this.$target.innerHTML = `
<div id="document-item" data-document-id="${id}">
<svg viewBox="0 0 12 12" class="arrow">
<path
d="M6.02734 8.80274C6.27148 8.80274 6.47168 8.71484 6.66211 8.51465L10.2803 4.82324C10.4268 4.67676 10.5 4.49609 10.5 4.28125C10.5 3.85156 10.1484 3.5 9.72363 3.5C9.50879 3.5 9.30859 3.58789 9.15234 3.74902L6.03223 6.9668L2.90722 3.74902C2.74609 3.58789 2.55078 3.5 2.33105 3.5C1.90137 3.5 1.55469 3.85156 1.55469 4.28125C1.55469 4.49609 1.62793 4.67676 1.77441 4.82324L5.39258 8.51465C5.58789 8.71973 5.78808 8.80274 6.02734 8.80274Z">
</path>
</svg>
<svg viewBox="0 0 30 30" class="page">
<g><path d="M16,1H4v28h22V11L16,1z M16,3.828L23.172,11H16V3.828z M24,27H6V3h8v10h10V27z M8,17h14v-2H8V17z M8,21h14v-2H8V21z M8,25h14v-2H8V25z"></path></g>
</svg>
<span class="title">${title}</span>
<svg viewBox="0 0 16 16" class="trash hidden">
<path d="M4.8623 15.4287H11.1445C12.1904 15.4287 12.8672 14.793 12.915 13.7402L13.3799 3.88965H14.1318C14.4736 3.88965 14.7402 3.62988 14.7402 3.28809C14.7402 2.95312 14.4736 2.69336 14.1318 2.69336H11.0898V1.66797C11.0898 0.62207 10.4268 0 9.29199 0H6.69434C5.56641 0 4.89648 0.62207 4.89648 1.66797V2.69336H1.86133C1.5332 2.69336 1.25977 2.95312 1.25977 3.28809C1.25977 3.62988 1.5332 3.88965 1.86133 3.88965H2.62012L3.08496 13.7471C3.13281 14.7998 3.80273 15.4287 4.8623 15.4287ZM6.1543 1.72949C6.1543 1.37402 6.40039 1.14844 6.7832 1.14844H9.20312C9.58594 1.14844 9.83203 1.37402 9.83203 1.72949V2.69336H6.1543V1.72949ZM4.99219 14.2188C4.61621 14.2188 4.34277 13.9453 4.32227 13.542L3.86426 3.88965H12.1152L11.6709 13.542C11.6572 13.9453 11.3838 14.2188 10.9941 14.2188H4.99219ZM5.9834 13.1182C6.27051 13.1182 6.45508 12.9336 6.44824 12.667L6.24316 5.50293C6.23633 5.22949 6.04492 5.05176 5.77148 5.05176C5.48438 5.05176 5.2998 5.23633 5.30664 5.50293L5.51172 12.667C5.51855 12.9404 5.70996 13.1182 5.9834 13.1182ZM8 13.1182C8.28711 13.1182 8.47852 12.9336 8.47852 12.667V5.50293C8.47852 5.23633 8.28711 5.05176 8 5.05176C7.71289 5.05176 7.52148 5.23633 7.52148 5.50293V12.667C7.52148 12.9336 7.71289 13.1182 8 13.1182ZM10.0166 13.1182C10.29 13.1182 10.4746 12.9404 10.4814 12.667L10.6934 5.50293C10.7002 5.23633 10.5088 5.05176 10.2285 5.05176C9.95508 5.05176 9.76367 5.22949 9.75684 5.50293L9.54492 12.667C9.53809 12.9336 9.72949 13.1182 10.0166 13.1182Z"></path>
</svg>
<span class="add-btn hidden"> + </span>
</div>
`;
};

this.init();
}
Loading