Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Mission5/임지선] Project_Notion_VanillaJs 과제 #50

Open
wants to merge 25 commits into
base: 4/#5_limjiseon
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
67775ff
[FEAT] 과제 세팅 완료 및 API 수정
Lim-JiSeon Jul 6, 2023
53e14ec
[FIX] Router 수정
Lim-JiSeon Jul 6, 2023
e741067
[FIX] API 통신 수정
Lim-JiSeon Jul 6, 2023
03f954a
[FIX] API 오류 해결
Lim-JiSeon Jul 6, 2023
21ab7c4
[FIX] 상태 관리 오류 해결 & 하위 문서 생성
Lim-JiSeon Jul 6, 2023
32d75bc
[FEAT] 하위 문서 수정 작업
Lim-JiSeon Jul 6, 2023
7668c85
[FIX] console.log 삭제
Lim-JiSeon Jul 6, 2023
a4ff1d7
[FEAT] 하위 문서 생성 및 바로가기 이동
Lim-JiSeon Jul 6, 2023
b14c73e
[FEAT] storage 삭제
Lim-JiSeon Jul 6, 2023
d3f3b9e
[FEAT] 한 화면에 목록과 편집기 구현
Lim-JiSeon Jul 6, 2023
037630a
[FIX] file rename
Lim-JiSeon Jul 6, 2023
612fa32
[FIX] LinkButton 파일 삭제 및 파일 생성 기능 수정
Lim-JiSeon Jul 6, 2023
722e43a
[FIX] API 정리
Lim-JiSeon Jul 7, 2023
80933e5
[FIX] editor 수정
Lim-JiSeon Jul 7, 2023
631e92f
[FEAT] 루트 파일 생성 코드 수정
Lim-JiSeon Jul 7, 2023
59b36a6
[FIX] postEdit 새로운 문서 생성 로직 수정
Lim-JiSeon Jul 7, 2023
35b68fd
[FEAT] 글 수정시 네비바 업데이트 기능 추가
Lim-JiSeon Jul 7, 2023
6853d66
[FIX] 함수 이름 변경
Lim-JiSeon Jul 7, 2023
09e3956
[FIX] 문서 트리 구조 구현
Lim-JiSeon Jul 7, 2023
e7b3dff
[FIX] id -> classname으로 통일
Lim-JiSeon Jul 7, 2023
bded9fe
[FIX] 코드 정리
Lim-JiSeon Jul 7, 2023
9c3557f
[STYLE] 디자인 구현
Lim-JiSeon Jul 7, 2023
0e0fec1
[FIX] element 추가 수정중(미해결)
Lim-JiSeon Jul 7, 2023
b6a613a
[FIX] 랜더링 오류 및 피드백 반영(1)
Lim-JiSeon Jul 19, 2023
ae82433
[FIX] 오류 수정 완료
Lim-JiSeon Jul 24, 2023
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
10 changes: 10 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html>
<head>
<title>Notion Project</title>
</head>
<body>
<main id="app"></main>
<link rel="stylesheet" href="style.css" />
<script src="/src/main.js" type="module"></script>
</body>
</html>
42 changes: 42 additions & 0 deletions src/components/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import PostEditPage from "./PostEditPage.js";
import PostSidebar from "./PostSidebar.js";
import { initRouter } from "../utils/router.js";

export default function App({ $target }) {
const $postSideBarContainer = document.createElement("div");
const $postEditContainer = document.createElement("div");
$target.appendChild($postSideBarContainer);
$target.appendChild($postEditContainer);
$postSideBarContainer.className = "post-side-bar-container";
$postEditContainer.className = "post-edit-container";

const postSideBar = new PostSidebar({
$target: $postSideBarContainer,
});

const postEditPage = new PostEditPage({
$target: $postEditContainer,
initialState: {
postId: "new",
post: {
title: "",
content: "",
},
},
});

this.route = () => {
const { pathname } = window.location;

postSideBar.setState();

if (pathname !== "/" && pathname.indexOf("/") === 0) {
const [, , postId] = pathname.split("/");
postEditPage.setState({ postId });
}
};

this.route();

initRouter(() => this.route());
}
51 changes: 51 additions & 0 deletions src/components/Editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export default function Editor({
$target,
initialState = {
title: "",
content: "",
},
onEditing,
}) {
const $editor = document.createElement("div");
$editor.className = "editor";

this.state = initialState;

let isInitialize = false;

$target.appendChild($editor);

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

this.render = () => {
if (!isInitialize) {
$editor.innerHTML = `
<input type="text" name="title" value="${this.state.title}" />
<textarea name="content">${this.state.content}</textarea>
`;
isInitialize = true;
}
};
this.render();

$editor.addEventListener("keyup", (e) => {
const { target } = e;

const name = target.getAttribute("name");

if (this.state[name] !== undefined) {
const nextState = {
...this.state,
[name]: target.value,
};
this.setState(nextState);
onEditing(this.state);
}
});
}
62 changes: 62 additions & 0 deletions src/components/PostEditPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { getData, putData } from "../utils/api.js";
import Editor from "./Editor.js";
import { pushRouter } from "../utils/router.js";

export default function PostEditPage({ $target, initialState }) {
const INTERVAL_SAVE_TIME = 2000;

this.state = initialState;

let timer = null;

const editor = new Editor({
$target,
initialState: {
title: "",
content: "",
},
onEditing: (post) => {
if (timer !== null) {
clearTimeout(timer);
}

timer = setTimeout(async () => {
const { postId } = this.state;
if (postId && postId !== "new") {
await putData(postId, post);
pushRouter(`/documents/${postId}`);
await getDocument();
}
}, INTERVAL_SAVE_TIME);
},
});

this.setState = async (nextState) => {
if (this.state.postId !== nextState.postId) {
this.state = nextState;
await getDocument();
return;
} else {
this.state = nextState;
}

editor.setState(
this.state.post || {
title: "",
content: "",
}
);
};

const getDocument = async () => {
const { postId } = this.state;
if (postId) {
const post = await getData(`/documents/${postId}`);

this.setState({
...this.state,
post,
});
}
};
}
54 changes: 54 additions & 0 deletions src/components/PostItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { deleteData, getData, postData } from "../utils/api.js";
import { pushRouter } from "../utils/router.js";

export default function PostItem(title, id) {
const $postItemBox = document.createElement("div");
$postItemBox.className = id;

const $li = document.createElement("li");
$li.className = id;

const $title = document.createElement("span");
$title.className = id;
$title.textContent = title;
$li.appendChild($title);

const $addButton = makeButton("+", id);
$li.appendChild($addButton);

const $removeButton = makeButton("-", id);
$li.appendChild($removeButton);

const $postSubItemBox = document.createElement("ul");

$postItemBox.appendChild($li);
$postItemBox.append($postSubItemBox);

$li.addEventListener("click", async (e) => {
const target = e.target;
if (target.closest("span") === $title) {
pushRouter(`/documents/${$title.className}`);
} else if (target.closest("button") === $addButton) {
const createdPost = await postData($addButton.className);
pushRouter(`/documents/${createdPost.id}`);
} else if (target.closest("button") === $removeButton) {
alert("문서가 정상적으로 삭제되었습니다.");
await deleteData($removeButton.className).then((res) => {
if (res.parent) pushRouter(`/documents/${res.parent.id}`);
else {
pushRouter(`/`);
location.reload();
}
});
}
});

return { $postItemBox, $postSubItemBox };
}

export const makeButton = (text, className) => {
const $button = document.createElement("button");
$button.textContent = text;
$button.className = className;
return $button;
};
32 changes: 32 additions & 0 deletions src/components/PostList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import PostItem from "./PostItem.js";

export default function PostList({ $target, initialState }) {
const $postList = document.createElement("ul");
$target.appendChild($postList);

this.state = initialState;

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

this.makeList = ($itemContainer, data) => {
data.forEach(({ title, documents, id }) => {
const { $postItemBox, $postSubItemBox } = PostItem(title, id);

$itemContainer.appendChild($postItemBox);

if (documents.length > 0) {
this.makeList($postSubItemBox, documents);
}
});
};

this.render = () => {
$postList.innerHTML = "";
this.makeList($postList, this.state);
};

this.render();
}
36 changes: 36 additions & 0 deletions src/components/PostSidebar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { getData, postData } from "../utils/api.js";
import PostList from "./PostList.js";
import { pushRouter } from "../utils/router.js";

export default function PostSidebar({ $target }) {
const $listContainer = document.createElement("div");
const $createButton = document.createElement("button");
const $title = document.createElement("h1");
$listContainer.className = "post-list-container";
$createButton.className = "create-button";
$title.className = "title";

const postList = new PostList({
$target: $listContainer,
initialState: [],
});

this.setState = async () => {
const documents = await getData("/documents");
postList.setState(documents);
this.render();
};

this.render = async () => {
$createButton.textContent = "문서 생성하기";
$title.textContent = "Notion Project";
$target.appendChild($title);
$target.appendChild($createButton);
$target.appendChild($listContainer);
};

$createButton.addEventListener("click", async () => {
const createdPost = await postData();
pushRouter(`/documents/${createdPost.id}`);
});
}
6 changes: 6 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import App from "./components/App.js";

const $target = document.querySelector("#app");
$target.className = "contentWrap";

new App({ $target });
49 changes: 49 additions & 0 deletions src/utils/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export const API_END_POINT = "https://kdt-frontend.programmers.co.kr";
export const API_X_USERNAME = "API_X_USERNAME_LIMJISEON";

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

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

throw new Error("API 처리중 에러가 발생했습니다.");
} catch (e) {
console.error(e);
}
};

export const getData = async (url) => {
return request(url);
};

export const postData = async (id = null) => {
return request("/documents", {
method: "POST",
body: JSON.stringify({
title: "제목 없음",
parent: id,
}),
});
};

export const deleteData = async (id) => {
return request(`/documents/${id}`, {
method: "DELETE",
});
};

export const putData = async (id, post) => {
return request(`/documents/${id}`, {
method: "PUT",
body: JSON.stringify(post),
});
};
23 changes: 23 additions & 0 deletions src/utils/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const ROUTE_CHANGE_EVENT_NAME = "route-change";

export const initRouter = (onRoute) => {
window.addEventListener(ROUTE_CHANGE_EVENT_NAME, (e) => {
const { nextUrl } = e.detail;

if (nextUrl) {
window.addEventListener("popstate", () => onRoute());
history.pushState(null, null, nextUrl);
onRoute();
}
});
};

export const pushRouter = (nextUrl) => {
window.dispatchEvent(
new CustomEvent(ROUTE_CHANGE_EVENT_NAME, {
detail: {
nextUrl,
},
})
);
};
Loading