JS_독서관리 시스템
프로젝트/소규모프로젝트들

JS_독서관리 시스템

728x90

이번에는 자바스크립트를 활용하여 간단한 독서 관리 시스템을 구현해 보겠다.

 

기능은 로그인,독서 등록, 삭제, 수정 등등 할수있는 간단한 기능으로 도전하였다.

 

 

먼저 로그인을 해서 받아와야 하는 기능을 추가할 것이므로 API와 서버에대한 아주 기초적인 지식이 필요하다. 하지만 몰라도 간단히 설명을 보면서 할 수 있을 것이다.

 

기능 구현을 위해서는 axios를 알아야한다.

https://github.com/axios/axios

 

GitHub - axios/axios: Promise based HTTP client for the browser and node.js

Promise based HTTP client for the browser and node.js - GitHub - axios/axios: Promise based HTTP client for the browser and node.js

github.com

 

axios 는 Promise객체이므로 then을 사용해야한다. 물론 await를 통하여 처리를 할 수 있다.

 

 

여러 브라우저 환경에서 API를 사용할 수 있게 하는 것이다. 많은 개발자들이 사용한다.

 

 

기본적인 템플릿은 bootstrap을 이용하여 있는 템플릿 양식을 이용하였다.  css도 bootstrap의 album css를 활용하였다.

1. 버튼에 이벤트 연결

2. 토큰 체크

3. 토큰으로 서버에서 나의 정보 받기

4. 나의 책을 서버에서 받기

5. 받아온 책 그리기

를 각각 비동기나 동기 함수를 이용하여 사용할 수 있도록 되어있다.

 

아직 서버나 api에 대한 개념이 별로 없는상태라 간단히 이해만 하고 넘어갔다.

 

git clone -b list https://github.com/2wonngjae/fc-js-project.git

 

설치를 받은 후에, cmd창을 열고 npm을 이용하여 서버를 간단히 열면 아래 서버에서 작업을 진행할 수 있다.

 

npm-i 와 npm start로 실행

 

 

 

bootstrap을 이용하여 가져온 템플릿을 살짝 변경한 main 페이지이다. 이제 위에서 말한 5가지 기능을 연결하는 방법을 간단히 서술해 보겠다.

 

 

우선 로그인 기능이 되어야 한다.

 

function getToken(){
    return localStorage.getItem('token');
}



async function login(event){
    event.preventDefault(); //더이상 submit에관련된 로직이 진행되지 않음
    event.stopPropagation(); // 상위로 submit이 전해지지 않도록 함

    const emailElement = document.querySelector('#email');
    const passwordElement = document.querySelector('#password');

    const email = emailElement.value;
    const password = passwordElement.value; //string으로 받아오게 된다

    try {
        const res = await axios.post('https://api.marktube.tv/v1/me', {
            email,   //email = email을 축약
            password,
        });

        const {token} = res.data; // const token = res.data.token;과 동일
        if (toekn === undefined){
            return;
        }
        localStorage.setItem('token',token);
        location.assign('/');
    } catch(error){
            const data = error.response.data;
            if (data){
                const state = data.error;
                if(state === 'USER_NOT_EXIST'){
                    alert('사용자가 존재하지 않습니다');
                } else if ( state === 'PASSWORD_NOT_MATCH') {
                    alert('비밀번호가 일치하지 않습니다.')
                }
            }
    }
}


function bindLoginButton(){
    const form = document.querySelector('#form-login'); //id가 form-login인 돔을 가져오게 된다.
    form.addEventListener('submit',login);  //submig이 들어오면 login 실행
}


async function main(){
    //버튼 이벤트 연결
    bindLoginButton();

    //토큰 체크
    const token = getToken();
    if (token !== null){
        location.assign('/');
        return;
    }
}


document.addEventListener('DOMContentLoaded', main);

 

로그인 버튼을 눌렀을때 버튼 이벤트가 연결되는 부분과, 토큰체크를 하는 부분으로 나누어서 버튼과 토큰이 일치해야지만 로그인이 가능하게 만들 수 있다.

 

위 JS코드를 적용시킨 후에 아까 npm으로 서버를 열었던 5000 주소로 가서 호출하면

위와같은 결과를 볼 수 있고, 해당 아이디에 비밀번호 1234가 일치할 경우에만 로그인이 가능하다.

 

 

 

그렇다면 책을 추가하는 기능도 넣어야 할 것이다. 이역시 html과 css를 준비한 후에, js만 추가하겠다.

 

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}`  //token값을 서버에서 확인 후 user의 정보를 줌
            }
        });
        return res.data;
    } catch(error){
        console.log('getUserByToken error',error);
        return null;
    }
}

async function save(event) {
    event.preventDefault();  //submit이 계속 진행되지 않도록
    event.stopPropagation(); //form을 가진 상위 돔에도 submit이 진행되지 않도록
    console.log('save');


    event.target.classList.add('was-validated'); //bootstrap기능, 이상이없다!

    const titleElement = document.querySelector('#title');
    const messageElement = document.querySelector('#message');
    const authorElement = document.querySelector('#author');
    const urlElement = document.querySelector('#url');

    const title = titleElement.value;
    const message = messageElement.value;
    const author = authorElement.value;
    const url = urlElement.value;

    if (title === '' || message === '' || author === '' || url === ''){
        return;
    }

    const token = getToken();
    if (token == null){
        location.assign('/login');
        return;
    }

    try {
        const res = await axios.post('https://api.marktube.tv/v1/book',{
            title,
            message,
            author,
            url
        }, {  //옵션으로 얻은 토큰도 전달한다.
            headers : {
                Authorization: `Bearer ${token}`
            }
        });
        location.assign('/');
    }catch(error){
        console.log('save error',error);
        alert('책 추가 실패');
    }

    
}



function bindSaveButton() {
    const form = document.querySelector('#form-add-book');
    form.addEventListener('submit',save)   //이벤트 연결

}


async function main() {
    //버튼에 이벤트 연결
    bindSaveButton();


    //토큰 체크

    const token = getToken();
    if(token === null){
        location.assign('/login');
        return;
    }

    //토큰으로 서버에서 나의 정보 받아오기
    const user = await getUserByToken(token);
    if (user === null) {
        localStorage.clear();      //localStorage파괴 및 login이동
        location.assign('/login');
        return;
    }
}


document.addEventListener('DOMContentLoaded', main);

 

방식이 로그인을 구현할때와 상당히 비슷하다. 토큰을 지속적으로 확인하면서, 버튼을 이벤트와 연결하고 빈 값들이 아니라면 내 정보를 넘겨주는것이다. 주석을 보면 더 자세할 것 같다.

 

 

 

중간에 event.target.classList.add('was-validated'); 코드를 통하여

 

 

위 그림처럼 다 입력하지않으면 빨간색 박스로 그려지는 기능을 추가할 수 있다.

 

 

 

그 다음으로는 저장한 책 정보를 보고, 삭제할 수 있는 부분이다. 이 역시 주석을 읽어보면서 정독해보는게 좋을 것 같다.

 

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 logout() {
    const token = getToken();
    if (token === null) {
      location = '/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();
      window.location.href = '/login';
    }
  }
  
  async function getBook(bookId) {
    const token = getToken();
    if (token === null) {
      location.href = '/login';
      return null;
    }
    try {
      const res = await axios.get(`https://api.marktube.tv/v1/book/${bookId}`, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
      return res.data;
    } catch (error) {
      console.log('getBook error', error);
      return null;
    }
  }
  
  async function deleteBook(bookId) {
    const token = getToken();
    if (token === null) {
      location = '/login';
      return;
    }
    await axios.delete(`https://api.marktube.tv/v1/book/${bookId}`, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
  }
  
  function bindLogoutButton() {
    const btnLogout = document.querySelector('#btn_logout');
    btnLogout.addEventListener('click', logout);
  }
  
  function render(book) {
    const detailElement = document.querySelector('#detail');
  
    detailElement.innerHTML = `<div class="card bg-light w-100">
      <div class="card-header"><h4>${book.title}</h4></div>
      <div class="card-body">
        <h5 class="card-title">"${book.message}"</h5>
        <p class="card-text">글쓴이 : ${book.author}</p>
        <p class="card-text">링크 : <a href="${
          book.url
        }" target="_BLANK">바로 가기</a></p>
        <a href="/edit?id=${book.bookId}" class="btn btn-primary btn-sm">Edit</a>
        <button type="button" class="btn btn-danger btn-sm" id="btn-delete">Delete</button>
      </div>
      <div class="card-footer">
          <small class="text-muted">작성일 : ${new Date(
            book.createdAt,
          ).toLocaleString()}</small>
        </div>
    </div>`;
  
    /* delete btn에 대한 기능 추가 */
    document.querySelector('#btn-delete').addEventListener('click', async () => {
      try {
        await deleteBook(book.bookId);
        location.href = '/';
      } catch (error) {
        console.log(error);
      }
    });
  }
  
  async function main() {
    // 버튼에 이벤트 연결
    bindLogoutButton();
  
    // 브라우저에서 id 가져오기
    const bookId = new URL(location.href).searchParams.get('id'); //book id를 가져옴
  
    // 토큰 체크
    const token = getToken();
    if (token === null) {
      location.href = '/login';
      return;
    }
  
    // 토큰으로 서버에서 나의 정보 받아오기
    const user = await getUserByToken(token);  //토큰의 유효성 판단
    if (user === null) {
      localStorage.clear();
      location.href = '/login';
      return;
    }
  
    // 책을 서버에서 받아오기
    const book = await getBook(bookId);
    if (book === null) {
      alert('서버에서 책 가져오기 실패');
      return;
    }
  
    // 받아온 책을 그리기
    render(book);
  }
  
  document.addEventListener('DOMContentLoaded', main);

 

위처럼 정보를 볼 수 있고, delete를 통하여 삭제할 수 있다.

 

 

마지막으로는 책의 정보를 수정할 수 있는 경우이다. 이역시 위에서 했던 부분과 거의 유사하다.

 

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 logout() {
  const token = getToken();
  if (token === null) {
    location = '/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();
    window.location.href = '/login';
  }
}

async function getBook(bookId) {
  const token = getToken();
  if (token === null) {
    location.href = '/login';
    return null;
  }
  try {
    const res = await axios.get(`https://api.marktube.tv/v1/book/${bookId}`, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    return res.data;
  } catch (error) {
    console.log('getBook error', error);
    return null;
  }
}

async function updateBook(bookId) {
  const titleElement = document.querySelector('#title');
  const messageElement = document.querySelector('#message');
  const authorElement = document.querySelector('#author');
  const urlElement = document.querySelector('#url');

  const title = titleElement.value;
  const message = messageElement.value;
  const author = authorElement.value;
  const url = urlElement.value;

  if (title === '' || message === '' || author === '' || url === '') {
    return;
  }

  const token = getToken();
  if (token === null) {
    location = '/login';
    return;
  }

  await axios.patch(
    `https://api.marktube.tv/v1/book/${bookId}`,
    {
      title,
      message,
      author,
      url,
    },
    {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    },
  );
}

function bindLogoutButton() {
  const btnLogout = document.querySelector('#btn_logout');
  btnLogout.addEventListener('click', logout);
}

function render(book) {
  const titleElement = document.querySelector('#title');
  titleElement.value = book.title;

  const messageElement = document.querySelector('#message');
  messageElement.value = book.message;

  const authorElement = document.querySelector('#author');
  authorElement.value = book.author;

  const urlElement = document.querySelector('#url');
  urlElement.value = book.url;

  const form = document.querySelector('#form-edit-book');
  form.addEventListener('click', async event => {
    event.preventDefault();
    event.stopPropagation();
    event.target.classList.add('was-validated');

    try {
      await updateBook(book.bookId);
      location.href = `book?id=${book.bookId}`;
    } catch (error) {
      console.log(error);
    }
  });

  const cancelButtonElement = document.querySelector('#btn-cancel');
  cancelButtonElement.addEventListener('click', event => {
    event.preventDefault();
    event.stopPropagation();

    location.href = `book?id=${book.bookId}`;
  });
}

async function main() {
  // 브라우저에서 id 가져오기
  const bookId = new URL(location.href).searchParams.get('id');

  // 토큰 체크
  const token = getToken();
  if (token === null) {
    location.href = '/login';
    return;
  }

  // 토큰으로 서버에서 나의 정보 받아오기
  const user = await getUserByToken(token);
  if (user === null) {
    localStorage.clear();
    location = '/login';
    return;
  }

  // 책을 서버에서 받아오기
  const book = await getBook(bookId);
  if (book === null) {
    alert('서버에서 책 가져오기 실패');
    return;
  }

  // 받아온 책을 그리기
  render(book);
}

document.addEventListener('DOMContentLoaded', main);

 

위처럼 책을 수정하는 페이지를 만들 수 있다..

 

 

아주 간단한 페이지를 구현해보면서 페이지 동작방식을 간단히 이해해 보았다. 아직 이해가 잘 안되는 부분도 많지만 더 공부하고 자료들을 잘 모아야겠다 생각했다.

 

 

728x90

'프로젝트 > 소규모프로젝트들' 카테고리의 다른 글

JS_리액트_TodoList  (0) 2021.12.27
MFC를 이용한 디지털신호처리 프로그램  (0) 2021.12.15
GitHub 클론코딩-2  (0) 2021.11.27
GitHub 클론코딩-1  (0) 2021.11.26
01_디지털 필터설계(C++)  (2) 2021.11.13