Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a3791c7
Add: "API end point 숨김"
mxx-kor Nov 16, 2022
dfcb1a5
Add: "추가 CSS, HTML, main.js"
mxx-kor Nov 16, 2022
b729d29
Feat: "추가 validation 검사 함수, router"
mxx-kor Nov 16, 2022
cc8da73
Feat: "최상위 컴포넌트 App"
mxx-kor Nov 16, 2022
58880ea
Feat: "추가 LandingPage"
mxx-kor Nov 16, 2022
e701bc6
Feat: "Sidebar 컴포넌트 추가"
mxx-kor Nov 16, 2022
ab4ada1
Feat: "EditPage 컴포넌트"
mxx-kor Nov 16, 2022
21e6fb6
Fix: "에러 수정"
mxx-kor Nov 16, 2022
5c66953
Fix: "state 변경 후 렌더하여 오류 수정"
mxx-kor Nov 16, 2022
9b976db
Refactor: "누락된 변수 사용"
mxx-kor Nov 25, 2022
d5ef51f
Fix: "새문서 작성시 breadcrumb 오류"
mxx-kor Nov 25, 2022
99ba91b
Refactor: "중복 바인딩 한줄로 처리"
mxx-kor Nov 25, 2022
44b6780
Refactor: "HTML inline style 속성을 CSS로 수정"
mxx-kor Nov 25, 2022
95f7373
Refactor: "append를 마지막에"
mxx-kor Nov 25, 2022
7b9451e
Feat: "선택된 사이드바 내용 변경 메서드 추가"
mxx-kor Nov 25, 2022
e686a19
Feat: "하위 페이지 생성과 삭제 시 호버 스타일 정리"
mxx-kor Nov 26, 2022
df7f49e
Refactor: "breadcrumb 방어코드, 메서드 분리, validation 추가"
mxx-kor Nov 26, 2022
eeb2c72
Refactor: "조건문 괄호 처리, validation 추가"
mxx-kor Nov 26, 2022
dca573d
Refactor: "사용하지 않는 파일 정리"
mxx-kor Nov 26, 2022
2949465
Refactor: "Editor 이벤트 디스트럭팅 변수 개선"
mxx-kor Nov 26, 2022
c86403e
Refactor: "SubLink 삼항연산자로 render 수정, 방어코드 추가"
mxx-kor Nov 26, 2022
e79b992
Refactor: "class로 display 속성 변경, 불필요한 async 삭제"
mxx-kor Nov 26, 2022
d38ffa4
Refactor: "btn 클래스 추가, css 변수 추가"
mxx-kor Nov 26, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
constants.js
storage.js
Copy link
Contributor

Choose a reason for hiding this comment

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

스토리지도 이그노어 하신 이유가 있나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

이번 프로젝트에서 storage를 사용하지 않아서 ignore했다가 지우는걸 잊었습니다.😓 리팩토링 진행하며 storage를 사용해볼 생각입니다.

14 changes: 14 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title>Minjae의 Notion</title>
</head>
<body>
<main id="app" style="display: flex; flex-direction: row"></main>
Copy link
Member

Choose a reason for hiding this comment

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

이 부분은 인라인 스타일로 지정한 이유가 무엇인가요? 👀

Copy link
Member Author

Choose a reason for hiding this comment

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

초반에 레이아웃을 보기위해 설정해놓고 잊고 있었습니다..🥲 리뷰 감사합니다~! 수정하겠습니다.

<script src="/src/main.js" type="module"></script>
</body>
</html>
82 changes: 82 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import EditorContainer from "./components/editpage/EditorContainer.js";
import Sidebar from "./components/sidebar/Sidebar.js";
import { request } from "./api.js";
import { initRouter, push } from "./router.js";
import { validation } from "./validation.js";
import LandingPage from "./components/editpage/LandingPage.js";

export default function App({ $target }) {
validation(new.target, "App");

this.state = {
documentsList: [],
editorDocument: {
docId: null,
doc: {
title: "",
content: "",
documents: [],
},
},
};

const sidebar = new Sidebar({
$target,
initialState: this.state.documentsList,
});

new LandingPage({ $target });

const editContainer = new EditorContainer({
$target,
initialState: this.state.editorDocument,
});
Comment on lines +11 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.

initial state 관리 방식 좋네요 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

감사합니다!🙇🏻‍♂️ 필요한 state들만 사용해보려고 노력해봤습니다.


this.setState = (nextState) => {
this.state = nextState;
editContainer.setState(this.state.editorDocument);
};

this.init = async () => {
const docs = await request("/documents");
this.setState({
...this.state,
documentsList: docs,
});
await sidebar.setState(docs);
this.route();
};

this.route = () => {
const { pathname } = window.location;
const editArea = document.querySelector(".editContainer");
const landingPage = document.querySelector(".landingPage");

if (pathname === "/") {
editArea.style.display = "none";
landingPage.style.display = "block";
} else if (pathname.indexOf("/documents/") === 0) {
editArea.style.display = "block";
landingPage.style.display = "none";
const [, , docId] = pathname.split("/");
//같은 문서 두번 클릭시 내용 삭제 방지
if (this.state.editorDocument.docId === docId) {
return;
}
//뒤로가기 대비 방어코드
if (docId === null) {
push("/");
}
this.setState({
...this.state,
editorDocument: { docId },
});
}
};

this.init();

window.addEventListener("popstate", () => this.route());

initRouter(() => this.route());
}
24 changes: 24 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { API_END_POINT } from "./constants.js";
import { X_USERNAME } from "./constants.js";
Copy link
Member

Choose a reason for hiding this comment

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

동일한 경로의 모듈에서 불러오는 바인딩은 한 줄로 처리해도 될 것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

미처 확인하지 못했습니다.. 리뷰 감사합니다!


export const request = async (url, options = {}) => {
try {
const res = await fetch(`${API_END_POINT}${url}`, {
...options,
headers: {
"x-username": X_USERNAME,
"Content-Type": "application/json",
},
});

if (res.ok) {
return await res.json();
}

throw new Error("API 처리중 뭔가 이상합니다!");
} catch (e) {
alert(e.message);
history.replaceState(null, null, "/");
location.reload();
}
};
59 changes: 59 additions & 0 deletions src/components/editpage/BreadCrumb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { validation } from "../../validation.js";

export default function BreadCrumb({ $target, initialState, clickPath }) {
validation(new.target, "BreadCrumb");

const $breadCrumb = document.createElement("nav");
$breadCrumb.className = "linkContainer";
$target.appendChild($breadCrumb);
Copy link
Contributor

Choose a reason for hiding this comment

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

엘리먼트의 내용을 작성하고 나서 append를 시키는 것도 좋아 보입니다. 커피챗 때 그렇게 해야 DOM의 조작이 줄어든다고 들었던 것 같습니다.


this.state = initialState;

this.setState = (nextState) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

state도 validation을 거치면 좋을 것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

전체적으로 validation 적용해보도록 하겠습니다. 감사합니다!

this.state = nextState;
this.render();
};

this.renderBreadCrumb = (id) => {
const path = [];

const recursiveBC = (id) => {
const $curLi = document.getElementById(id);
const $ul = $curLi.closest("ul");

if ($ul.className === "child") {
path.push([$curLi.querySelector("span").innerHTML, id]);

Copy link

Choose a reason for hiding this comment

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

Suggested change
const $curLi = document.getElementById(id);
const $ul = $curLi.closest("ul");
if ($ul.className === "child") {
path.push([$curLi.querySelector("span").innerHTML, id]);
const $curLi = document.getElementById(id);
if ($curLi) {
const $ul = $curLi.closest("ul");
if ($ul.className === "child") {
path.push([$curLi.querySelector("span").innerHTML, id]);

$curLi가 존재하는가에 대한 방어코드를 넣는것도 좋을듯 합니다

Copy link
Member Author

Choose a reason for hiding this comment

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

해당 컴포넌트에서는 방어코드가 없었는데 중요한 부분을 잘 잡아주신것 같습니다. 리뷰 반영해보도록 하겠습니다! 감사합니다👍

recursiveBC($ul.closest("li").id);
} else {
path.push([$curLi.querySelector("span").innerHTML, id]);
}
};
Copy link
Member

Choose a reason for hiding this comment

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

이 함수 정의 부분을 별도의 메서드로 분리해도 좋을 것 같네요.

Copy link
Member Author

Choose a reason for hiding this comment

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

생각해보니 함수안에 정의할 필요가 없는 것 같습니다😓 함수를 밖으로 빼서 분리하는 리뷰 반영해보겠습니다. 감사합니다!


recursiveBC(id);

return path
.reverse()
.map((el) => `<span class="link" data-id=${el[1]} >${el[0]}</span>`)
.join(" / ");
Comment on lines +37 to +40
Copy link
Member

Choose a reason for hiding this comment

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

👍

};

$breadCrumb.addEventListener("click", (e) => {
const $span = e.target.closest("span");

if ($span) {
const { id } = $span.dataset;
clickPath(id);
}
});

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

$breadCrumb.innerHTML = `
<div>
${docId ? this.renderBreadCrumb(docId) : ""}
</div>
`;
};
}
46 changes: 46 additions & 0 deletions src/components/editpage/Editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { validation } from "../../validation.js";

export default function Editor({ $target, initialState, onEdit }) {
validation(new.target, "Editor");
const $editor = document.createElement("div");
$editor.className = "editor";
$target.appendChild($editor);

$editor.innerHTML = `
<div style="display: flex; flex-direction: column">
<input type="text" placeholder="제목 없음" name="title" style="height:100px" />
<textarea name="content" placeholder="내용을 입력하세요" style="height:75vh;"></textarea>
</div>
`;

this.state = initialState;

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

this.render = () => {
$editor.querySelector("[name=title]").value = this.state.title;
$editor.querySelector("[name=content]").value = this.state.content;
};

this.render();

$editor.addEventListener("keyup", (e) => {
const targetTag = e.target.tagName;
const { value } = e.target;
Copy link
Contributor

Choose a reason for hiding this comment

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

e.target을 공통적으로 사용하므로 const {target} = e 로 꺼내서 사용할 수 있겠네요.

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const targetTag = e.target.tagName;
const { value } = e.target;
const { tagName: targetTag, value } = e.target;

let nextState = {};
if (targetTag === "INPUT") {
nextState = { ...this.state, title: value };
} else {
nextState = {
...this.state,
content: value,
};
}

this.setState(nextState);
onEdit(this.state);
});
}
80 changes: 80 additions & 0 deletions src/components/editpage/EditorContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { request } from "../../api.js";
import BreadCrumb from "./BreadCrumb.js";
import Editor from "./Editor.js";
import { push } from "../../router.js";
import SubLink from "./SubLink.js";
import { validation } from "../../validation.js";

//여기에선 docId만 핸들, Editor에는 Doc만
export default function EditorContainer({ $target, initialState }) {
validation(new.target, "EditorContainer");

const $editorContainer = document.createElement("section");
$editorContainer.className = "editContainer";

this.state = initialState;

let timer = null;

const breadCrumb = new BreadCrumb({
$target: $editorContainer,
initialState: this.state,
clickPath: (id) => {
push(`/documents/${id}`);
},
});

const editor = new Editor({
$target: $editorContainer,
initialState: this.state.doc,
onEdit: (post) => {
document.getElementById(post.id).getElementsByTagName("span")[0].innerText = post.title;

if (timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(async () => {
await request(`/documents/${post.id}`, {
method: "PUT",
body: JSON.stringify(post),
});
}, 500);
},
});

const subLink = new SubLink({
$target: $editorContainer,
initialState: this.state,
clickLink: (id) => {
push(`/documents/${id}`);
},
});

this.setState = async (nextState) => {
if (this.state.docId !== nextState.docId) {
this.state = nextState;
await fetchDoc();
return;
}
this.state = nextState;

breadCrumb.setState(this.state);
subLink.setState(this.state);
editor.setState(this.state.doc || { title: "", content: "" });
this.render();
};

this.render = () => {
$target.appendChild($editorContainer);
};

const fetchDoc = async () => {
const { docId } = this.state;
const doc = await request(`/documents/${docId}`);

this.setState({
...this.state,
doc,
});
};
}
20 changes: 20 additions & 0 deletions src/components/editpage/LandingPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { validation } from "../../validation.js";

export default function LandingPage({ $target }) {
validation(new.target, "LandingPage");

const $landingPage = document.createElement("section");
$landingPage.className = "landingPage";
$target.appendChild($landingPage);

this.render = () => {
$landingPage.innerHTML = `
<h1 class="landingTitle">
Notion에 오신 것을 환영합니다.
</h1>
<p class="landingP">페이지를 추가하여 새 문서를 작성해보세요.</p>
`;
};

this.render();
}
38 changes: 38 additions & 0 deletions src/components/editpage/SubLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { validation } from "../../validation.js";
export default function SubLink({ $target, initialState, clickLink }) {
validation(new.target, "SubLink");

const $subLink = document.createElement("footer");
$subLink.className = "linkContainer";
$target.appendChild($subLink);

this.state = initialState;

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

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

if (doc.documents.length > 0) {
$subLink.innerHTML = doc.documents
.map((el) => `<div class="link" data-id=${el.id}>${el.title}</div>`)
.join("");
} else {
$subLink.innerHTML = `
`;
}
Copy link
Member

Choose a reason for hiding this comment

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

이 경우는 삼항 연산자도 좋을 것 같네요.

};

$subLink.addEventListener("click", (e) => {
const { id } = e.target.closest("div").dataset;
const $curLi = document.getElementById(id);
const $parentUl = $curLi.closest("ul");

$parentUl.closest("li").querySelector(".toggleFold").innerText = "▼";
$parentUl.style.display = "block";
clickLink(id);
});
}
Comment on lines +29 to +36
Copy link

Choose a reason for hiding this comment

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

크기가 작고 컴포넌트이고 클릭되는 것이 제한되어 있지만, 그래도 나중에 코드가 추가되는 경우를 생각하여(지금은 그럴일은 없지만😂) 방어코드가 있으면 조금 더 좋지 않을까 싶습니다

Loading