Login - 아이디와 비밀번호를 입력하는 화면
List - 사용자가 추가한 책들의 목록을 보여주는 화면
ADD - 책을 추가하는 화면
Detail - 책의 상세내용 보여주는 화면
Edit - Detail에서 책의 내용을 수정하는 화면
사용자가 추가한 책들의 목록을 보여주는 화면으로, 부트스트랩의 example 중에서 album 템플릿을 사용해서 구현했다.
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<title>Book example</title>
</head>
<body>
<header>
<div class="collapse bg-dark" id="navbarHeader">
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-7 py-4">
<h4 class="text-white">About</h4>
<p class="text-muted">Add some information about the album below, the author, or any other
background context. Make it a few sentences long so folks can pick up some informative
tidbits. Then, link them off to some social networking sites or contact information.</p>
</div>
<div class="col-sm-4 offset-md-1 py-4">
<h4 class="text-white">Contact</h4>
<ul class="list-unstyled">
<li><a href="#" class="text-white">Follow on Twitter</a></li>
<li><a href="#" class="text-white">Like on Facebook</a></li>
<li><a href="#" class="text-white">Email me</a></li>
<li><button id="btn_logout">로그아웃</button></li>
</ul>
</div>
</div>
</div>
</div>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a href="#" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="me-2"
viewBox="0 0 24 24">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" /></svg>
<strong>Album</strong>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarHeader"
aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
</header>
<main>
<section class="py-5 text-center container">
<div class="row py-lg-5">
<div class="col-lg-6 col-md-8 mx-auto">
<h1 class="fw-light">Book example</h1>
<p class="lead text-muted">Something short and leading about the collection below—its contents, the
creator, etc. Make it short and sweet, but not too short so folks don’t simply skip over it
entirely.</p>
<p>
<a href="#" class="btn btn-primary my-2">Add Book</a>
</p>
</div>
</div>
</section>
<div class="album py-5 bg-light">
<div class="container">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225"
xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail"
preserveAspectRatio="xMidYMid slice" focusable="false">
<title>Placeholder</title>
<rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
dy=".3em">Thumbnail</text>
</svg>
<div class="card-body">
<p class="card-text">모던 자바스크립트 입문</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225"
xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail"
preserveAspectRatio="xMidYMid slice" focusable="false">
<title>Placeholder</title>
<rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
dy=".3em">Thumbnail</text>
</svg>
<div class="card-body">
<p class="card-text">클린 아키텍쳐: 소프트웨어 구조와 설계의 법칙</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225"
xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail"
preserveAspectRatio="xMidYMid slice" focusable="false">
<title>Placeholder</title>
<rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
dy=".3em">Thumbnail</text>
</svg>
<div class="card-body">
<p class="card-text">Head First Agile</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225"
xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail"
preserveAspectRatio="xMidYMid slice" focusable="false">
<title>Placeholder</title>
<rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
dy=".3em">Thumbnail</text>
</svg>
<div class="card-body">
<p class="card-text">리액트를 다루는 기술</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225"
xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail"
preserveAspectRatio="xMidYMid slice" focusable="false">
<title>Placeholder</title>
<rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
dy=".3em">Thumbnail</text>
</svg>
<div class="card-body">
<p class="card-text">Node.js 교과서</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225"
xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail"
preserveAspectRatio="xMidYMid slice" focusable="false">
<title>Placeholder</title>
<rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
dy=".3em">Thumbnail</text>
</svg>
<div class="card-body">
<p class="card-text">러닝 자바스크립트</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="text-muted py-5">
<div class="container">
<p class="float-end mb-1">
<a href="#">Back to top</a>
</p>
<p class="mb-1">Album example is © Bootstrap, but please download and customize it for yourself!</p>
<p class="mb-0">New to Bootstrap? <a href="/">Visit the homepage</a> or read our <a
href="/docs/5.1/getting-started/introduction/">getting started guide</a>.</p>
</div>
</footer>
<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous">
</script>
<script src="https://unpkg.com/browse/axios@0.2.1/dist/axios.min.js"></script>
<script src="js/index.js"></script>
</body>
</html>
index.js
function bindLogoutButton() {
const btnLogout = document.querySelector("#btn_logout");
btnLogout.addEventListener('click', logout);
}
async function logout() {
const token = getToken();
if (token === null) {
location.assign('/login');
return;
}
try {
await axios.delete('https://api.marktube.tv/v1/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
} catch (error) {
console.log('logout error', error);
} finally {
localStorage.clear();
location.assign('/login');
}
}
function getToken() {
return localStorage.getItem('token');
}
async function getUserByToken(token) {
try {
const res = await axios.get('https://api.marktube.tv/v1/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
return res.data;
} catch (error) {
console.log('getUserByToken error', error);
return null;
}
}
async function getBooks(token) {
try {
const res = await axios.get('https://api.marktube.tv/v1/book', {
headers: {
Authorization: `Bearer ${token}`,
},
});
return res.data;
} catch (error) {
console.log('getBooks error', error);
return null;
}
}
function render(books) {
const listElement = document.querySelector('#list')
for (let i = 0; i < books.length; i++) {
const book = books[i]
const bookElement = document.createElement('div');
bookElement.classList.value = 'col'
bookElement.innerHTML = `
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text">${book.title === '' ? '제목없음' : book.title}</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<a href="/book?id=${book.bookId}">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
</a>
<button type="button" class="btn btn-sm btn-outline-secondary btn-delete"
data-book-id="${book.bookId}">Delete</button>
</div>
<small class="text-muted">${new Date(
book.createdAt,
).toLocaleString()}</small>
</div>
</div>
</div>
`;
listElement.append(bookElement);
}
document.querySelectorAll('.btn-delete').forEach(element => {
element.addEventListener('click', async event => {
const bookId = event.target.dataset.bookId;
try {
await deleteBook(bookId);
location.reload();
} catch (error) {
console.log(error);
}
});
});
}
async function deleteBook(bookId) {
const token = getToken();
if (token === null) {
location.assign('/login');
return;
}
await axios.delete(`https://api.marktube.tv/v1/book/${bookId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return;
}
async function main() {
//버튼에 이벤트 연결
bindLogoutButton();
//토큰 체크
const token = getToken();
if (token === null) {
location.assign('/login');
return;
}
//토큰으로 서버에서 나의 정보 받아오기
const user = await getUserByToken(token);
if (user === null) {
localStorage.clear();
location.assign('/login');
return;
}
//나의 책을 서버에서 받아오기
const books = await getBooks(token);
if (books === null) {
return;
}
// 받아온 책을 그리기
render(books);
}
document.addEventListener('DOMContentLoaded', main);
프로젝트를 완료 한뒤에 추가로 작성해 놓으면 좋을것 같아서 작성한다.
아래의 결과는 내가 DOMContentLoaded
설정을 하지 않은 경우의 index.html
화면 이미지이다.
render(books)
함수로 출력되어있어야 할 책의 목록들이 보이지 않는 현상이 일어났다.
html에 script도 맨 아래에 적었는데 왜?
이유는 간단하다. render(books)
는 index.html에서 list
라는 id 값을 가진 엘리먼트 내부에 반복적으로 출력될 책의 목록을 구현해 놓았기 때문이다.
DOMContentLoaded를 추가 하지 않으면, index.html의 DOM이 생성되기 전에 DOM 내부에 엘리먼트를 가져오는 일이 생기기 때문에 render(books)
자체가 실행되지 않은것이다.
내가 html의 요소를 가져오고, 그 요소를 이용해서 구현하는 함수가 존재 했기 때문에, 브라우저가 HTML을 전부다 읽고 DOM트리를 만든 뒤에 발생하게끔 수정을 했어야 한다는 뜻이 된다.
그냥 '이걸 적어야한다' 하고 그러려니 하고 넘어갔으면 이런 원리를 깨우치지 못했을 것이다. 자신이 개발중에 의문점이 드는 것이 있으면 바로 찾아서 해결해 보는것이 좋은 경험이라고 생각한다.