Frontend stack : html, css, javascript
Backend stack : Flask, MongoDB, API, python

프로젝트 설명 : 서울시에서 제공하는 Open API 를 이용하여 서울 도서관 인기대출 도서목록 20선 정보의 데이터를 가져와 책에 대한 리뷰 작성을 할 수 있는 서비스를 제공하는 웹 플랫폼이다.

Open API : https://data.seoul.go.kr/dataList/OA-15475/S/1/datasetView.do

와이어프레임 : figjam 서비스를 이용해서 개발해야 하는 기능들을 도식화 하였다.


1.메인 페이지

  • 페이지 설명 : 서버를 실행하면 메인 페이지가 보이게 되는데 상단 바에 로그인과 회원가입 기능이 있는 버튼을 추가 하였고 Open API의 key값을 생성하고 fetch 를 이용하여 데이터를 json 형식으로 가져와 인기 순서대로 보이게 하였다.

    서버 로딩시 보여지는 페이지는 로그인이 되지 않으면 책의 클릭시 리뷰를 쓸 수 있는 페이지로 넘어가지지 않도록 구현하였다.

구현 기능 #1 : fetch를 이용한 json 데이터 가져오기

    <script>
        let login_check=0

        $(document).ready(function () {
            fetch("http://openapi.seoul.go.kr:8088/4a7869566f73656138317157546a58/json/SeoulLibraryBookRentNumInfo/1/20/")
            .then(res => res.json()).then(data => {
                rows= data['SeoulLibraryBookRentNumInfo']['row']
                rows.forEach((a)=>{
                    let title = a['TITLE']
                    let author = a['AUTHOR']
                    let publisher = a['PUBLISHER']
                    let publisher_year = a['PUBLISHER_YEAR']

                    console.log(title,author,publisher,publisher_year)

                    temp_html=`                    <button class = "my-transparent-button"token interpolation">${title}','${author}','${publisher}','${publisher_year}')">
                            <div class="col">
                                <div class="card">
                                    <div class="card-body">
                                        <h5 class="card-title">${title}</h5>
                                        <p class="card-text">${author}</p>
                                        <p>${publisher}</p>
                                        <p >${publisher_year}</p>
                                    </div>
                                </div>
                            </div>
                        </button>`                 
                    $('#cards').append(temp_html)
                })
            })
    </script>

구현 기능 #2 : 로그인 하기(front)

			function login() {
                $.ajax({
                    type: "POST",
                    url: "/login",
                    data: {id_give: $('#id').val(), pw_give: $('#pw').val()},
                    success: function (response) {
                        if (response['result'] == 'success') {
                            $.cookie('mytoken', response['token']);
                            alert('로그인 완료!')
                            location.href="/main_login_success"
                        } else {
                            // 로그인이 안되면 에러메시지를 띄웁니다.
                            alert(response['msg'])
                        }
                    }
                })
            }

코드 설명 : 메인 페이지에서 입련된 아이디와 비밀번호를 서버쪽에 보내어 결과값을 응답받고 response 값이 success라면 로그인 성공 페이지인 "/main_login_succsee" 페이지로 보낸다.
로그인에 실패 했다면 에러 메세지를 띄운다.

구현 기능 #3 : 로그인 하기 (back)

  @app.route('/login', methods=['POST'])
  def api_login():
      id_receive = request.form['id_give']
      pw_receive = request.form['pw_give']    
      pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()
      result = db.mini_project.find_one({'id': id_receive, 'pw': pw_hash})
      if result is not None:
          payload = {
              'id': id_receive,
              'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=600),
              'nickname': result['nick']
          }
          token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
          return jsonify({'result': 'success', 'token': token })

      else:
          return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})

코드 설명 : 클라이언트로 부터 받은 아이디와 비밀번호를 db에 저장된 값과 비교하여 result값이 빈 값이 아니라면 토큰을 만들어 발급한다. 이때 클라이언트로 부터 받은 비밀번호 값은 sha256 방법으로 암호화 되어 있다. 마찬가지로 토큰도 같은 방식으로 암호화 한다.
result 값이 없다면 에러 메세지와 'result' 값을 'fail'로 리턴한다.

구현 기능 #4 : 로그인 성공 페이지로 보내기 (back)

  @app.route("/main_login_fail", methods=["GET"])
  def main_login_fail():
      print('main_login_fail')
      return render_template('./main_login_fail.html')

코드 설명 : 클라이 언트에서 "location.href="/main_login_success"" 코드가 실행되면 서버에서 저장된 html 을 불러 오는 것이다. 다른 비슷한 서버 코드는 생략한다.


2. 회원가입 페이지

구현 기능 #1 : 회원가입을 통해 정보 DB에 저장 (front)

<script>
        function join(){
            try{
                checkPassword()
                $.ajax({
                    type: "POST",
                    url: "/api/join",
                    data: {
                        id_give: $('#signupName').val(),
                        pw_give: $('#signupPassword').val(),
                        nickname_give: $('#signupEmail').val()
                    },
                    success: function (response) {
                        if (response['result'] == 'success') {
                            alert('회원가입이 완료되었습니다.')
                            window.location.href = '/'
                        } else {
                            alert(response['result'])
                        }
                    }
                })
            }catch(e){
                alert('비밀번호를 확인해 주세요')
                console.log("잘못된 입력입니다.")
            }
        }
        
        //비밀번호 일치 확인
        function checkPassword(){
            let password = $('#signupPassword').val()
            let again_password = $('#signupPasswordagain').val()
            if(password != again_password){
                throw e
            }
        }

        function back(){
            window.location.href = '/'
        }

    </script>

코드 설명 : 메인 페이지에서 Join 버튼을 누르게 되면 회원가입 페이지로 들어오며 서버에 POST형식으로 작성된 정보를 보낸다.
checkPassword() 함수를 통해 비밀번호 확인 기능을 구현하고 같지 않으면 error를 던저 경고창을 띄운다.

구현 기능 #2 : 회원가입을 통해 정보 DB에 저장 (back)

  @app.route('/join', methods=['GET'])
  def register():
      return render_template('join.html')

  @app.route('/api/join', methods=['POST'])
  def api_register():
      print('api_register')
      id_receive = request.form['id_give']
      pw_receive = request.form['pw_give']
      nickname_receive = request.form['nickname_give']

      if(id_receive == "" or pw_receive == "" or nickname_receive == ""):
          return jsonify({'result': '항목이 누락되었습니다.'})

      pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()

      print('+++++++++++++++++++++')
      print(id_receive,pw_receive,nickname_receive)

      db.mini_project.insert_one({
          'id': id_receive,
          'pw': pw_hash, 
          'nick': nickname_receive
      })

      return jsonify({'result': 'success'})

코드 설명 : 클라이언트로 부터 받은 회원가입 정보를 정보가 누락 되었는지 if 문으로 확인하고 없다면 DB에 저장한뒤 'success' 값을 'result' 에 담아서 리턴한다. 만약 정보가 누락 되었다면 'result'의 값에 경고문구를 담아 리턴한다.
여기서 비밀번호 값은 sha256 방법(단반향 암호화, 풀어볼 수 없음)으로 암호화 하여 저정한다.


3.로그인 성공 페이지


로그인 성공페이지는 "main.login.success.html" 페이지로 "main.login.fail.html" 페이지와는 다르게 책을 클릭시 리뷰를 쓸 수 있는 페이지로 넘어갈 수 있는 기능을 가진다.

구현 기능 #1 : 리뷰를 쓸 수 있는 페이지로 전환 (front)

<button class = "my-transparent-button" onclick="reviews('${title}','${author}','${publisher}','${publisher_year}')">

function reviews(title,author,publisher,publisher_year){
	console.log(title,author,publisher,publisher_year)
    	location.href="reviews?" + title + "/" + author + "/" + publisher+"/"+publisher_year
}

코드 설명 : 리뷰페이지로 넘어가기전에 선택된 책의 정보를 가지고 들어가야 한다. html 에서 js로 매개변수를 보내는 과정에서 문제가 발생하였고 데이터 값을 URL에 붙여서 전송하는 GET 방식을 사용하였다. reviews? 에서 '?' 뒤에 파라미터를 붙여주는 방식이다.


4. 책에 대한 리뷰 페이지

  • 페이지 설명 : 메인페이지에서 로그인후 책을 클릭하게 되면 그림과 같이 리뷰를 작성할 수 있는 reviews.html 페이지로 들어오게 된다. 상단에는 책에 대한 정보가 기입 되어있고 닉네임 과 리뷰를 작성하고 SubMit 버튼을 누르게 되면 책의 제목, 닉네임, 리뷰가 DB에 저장되어 각 책마다 사용자가 작성한 서로 다른 리뷰를 볼 수 있게 구현 하였다.

구현 기능 #1 : encoding 데이터 decoding 하기 (front)

<script>
        let title, author, publisher, publisher_year

        $(document).ready(function () {
            // "reviews?" + title + "/" + author + "/" + publisher+"/"+publisher_year
            temp = location.href.split("?");
            // "reviews"
            // title + "/" + author + "/" + publisher+"/"+publisher_year

            data = temp[1].split("/");
            // title 
            // author 
            // publisher
            // publisher_year    

            title = decodeURI(data[0])
            author = decodeURI(data[1])
            publisher = decodeURI(data[2])
            publisher_year = decodeURI(data[3])
            console.log(title, author, publisher, publisher_year)

            $('#book-title').text(title)
            $('#author').text(author)
            $('#publisher').text(publisher)
            $('#publisher_year').text(publisher_year)

            show_comments()
        })
</script>

코드 설명 : 리뷰 작성 페이지에 들어오면 ready() 함수에 의해 GET 방식으로 URL에 붙은 parameter 를 split("?") 함수로 나누고 "/"를 기준으로 서로 다른 데이터들을 data 변수에 담아 사람이 볼 수 있도록 decodeURI() 함수를 이용하여 decoding 한다.
decoding 된 데이터들을 전연변수로 선언된 title, author, publisher, publisher_year를 이용하여 사용자에게 보여준다. show_commnets() 함수를 호출하면서 끝난다.

구현 기능 #2 : DB에 저장된 각 책의 리뷰 보여주기 (front)

<script>
        function show_comments() {
            // /$('#review-list').empty()
            fetch('/comments_show').then((res) => res.json()).then((data) => {
                rows = data['result']
                rows.forEach((n) => {
                    console.log(n)
                    if(n['title']==title){
                        let title = n['title']
                        let comments= n['user_comment_receive']
                        let nick_name=n['nick_name']

                        console.log(nick_name,comments,title)
                        let temp_html=
                        `
                            <div class = "box">
                                <strong>${nick_name}:</strong> ${comments}    
                            </div>

                        `
                        $('#review_list').append(temp_html)                   
                    }               
                })
            })
        }
</script>

코드설명 : 페이지에 들어오면 위에 코드도 바로 실행되어 DB에 저장된 각 책마다 해당되는 리뷰가 보여진다. fetch 를 이용하여 서버에서 DB에 있는 데이터를 가져와 전역변수로 선언된 title 과 매칭 되는 책의 제목을 찾아 DB에 저장되어 있는 모든 닉네임과 리뷰를 보여준다.

구현 기능 #3 : 작성된 리뷰 서버로 보내주기(front)

    <script>
        function submitReview() {
            const username = document.getElementById('username').value;
            const userReview = document.getElementById('review').value;

            console.log("submit_review")
            $.ajax({
                type: "POST",
                url: "/save_comment",
                data: {
                    book_title: title,
                    nick_name: username,
                    user_comment: userReview
                },
                success: function (response) {
                    if (response['msg'] == 'success') {
                        alert('댓글 등록이 완료되었습니다.')
                        window.location.reload()
                    } else {
                        alert(response['msg'])
                    }
                }
            })
            
        }
    </script>

코드설명 : 사용자가 작성한 닉네임과 리뷰를 가져와서 서버에 POST 형식으로 body에 담에 데이터를 보내고 서버에서 'msg' 값이 'success' 라면 페이지가 reload() 되도록 구현하였고 아니라면 경고 메세지를 띄운다.


구현 기능 #4 : DB에 사용자 리뷰정보 저장 및 데이터 가져오기(back)

  @app.route('/save_comment', methods=['POST'])
  def save_comment():
      print('save_commnet')
      title_receive=request.form['book_title']
      nick_name_receive = request.form['nick_name']
      user_comment_receive = request.form['user_comment']

      print(title_receive,nick_name_receive,user_comment_receive)
      if(nick_name_receive == "" or user_comment_receive == "" ):
          return jsonify({'msg': '항목이 누락되었습니다.'})

      db.mini_project.insert_one({
          'title': title_receive,
          'nick_name': nick_name_receive,
          'user_comment_receive':user_comment_receive
      })

      return jsonify({'msg': 'success'})

  @app.route("/comments_show", methods=["GET"])
  def comments_show():
      print('comments_show')
      all_comments_data = list(db.mini_project.find({},{'_id':False}))
      return jsonify({'result': all_comments_data})

코드 설명 :
1. save_comment() 함수의 경우 클라이언트로부터 받은 데이터를 이용하여 리뷰 작성시 닉네임 또는 작성된 리뷰가 비어 있는지 확인하고 비어 있다면 'msg' 값에 경고 메시지를 담아준뒤 리턴한다. 아니라면 DB에 저장후 'msg' 값에 'success' 를 리턴해준다.
2. comments_show() 함수의 경우 DB에 있는 모든 데이터를 list형식으로 'result' 에 담에 리턴한다.


Trouble shooting

문제 #1

  • 문제 상황
<form method="POST" action="#" role="form">
	<div class="form-group">
    	<button onclick="join()" id="signupSubmit" type="submit" class="btn btn-info btn-block">완료</button>
    	<button onclick="back()" id="backtoMain" type="submit" class="btn btn-info btn-block">메인으로</button>
	</div>
	<hr>
</form>

회원가입 폼에서 완료 버튼을 누르게 되면 메인 페이지로 이동 하는 것이 아니라 url에 #이 붙고 페이지 이동이 되지 않았다.

  • 해결 과정
    회원가입 폼에서 작성된 정보는 DB에 저장되는 것이고 POST방식이 아닌 GET방식으로 메인페이지로 이동하는 방식이고 action 속성은 서버로 보낼때 데이터가 도착할 URL을 명시하는 특징을 가지고 있어서 버튼을 누르면 join.html#으로 URL이 변하게 된다. 때문에 해당부분들을 지워서 해결해 나갔다.
  • 해결 코드
<form>
</form>

문제 #2

  • 문제 상황
  @app.route("/comments_show", methods=["POST"])
  def comments_show():
      title_receive = request.form['title']
      all_comments_data = list(db.mini_project.find({'title' : title_receive},{'_id':False}))
      return jsonify({'result': all_comments_data})

해당 클라이언트에서 받은 title에 대한 값을 title_receive에 저장하며 DB에 저장된 'title'과 비교하여 해당되는 list를 받아 반환하는 과정에서 클라이언트에서 받은 변수를 인식하지 못하는 문제가 발생하였다.

  • 해결 과정
    서버로 title변수를 넘겨주는것이 아니라 서버에서 전체 list를 리턴받아서 클라이언트 쪽에서 비교하여 반복문을 통해 해당하는 값들을 출력시켜 주었다.
  • 해결 코드
  @app.route("/comments_show", methods=["GET"])
  def comments_show():
      print('comments_show')
      all_comments_data = list(db.mini_project.find({},{'_id':False}))
      return jsonify({'result': all_comments_data})
	rows.forEach((n) => {
                    console.log(n)
                    if(n['title']==title){...}

문제 #3

  • 문제 상황
    main_login_success.html 에서 reviews() 함수에서 location.href="reviews?" + parameter 코드의 실행으로 js의 페이지 이동이 발생하는데 parameter 부분이 encoding 되어 reviews.html 부분에서 잘못된 값으로 저장되어 값이 출력되지 않는 문제가 발생했다.
  • 해결 과정
    UTF-8 코드 단위(1바이트)로 처리하여 URI를 디코딩하는 decodeURI() 함수를 이용하여 GET방식으로 온 parameter 들을 처리하였다.
  • 해결 코드
	temp = location.href.split("?");
	data = temp[1].split("/");
    // title 
    title = decodeURI(data[0])

문제 #4

  • 문제 상황
  <button class = "my-transparent-button" onclick=reviews(${title},${author})>

두개 이상의 변수를 함수로 보내줄때 해당 변수를 인식하지 못하는 상황이 발생하였다.

  • 해결 과정
    각각의 변수 앞에 따음표로 감싸주어야 인식하는 것을 구글링을 통해서 알 수 있었다.
    또한 js를 이용해서 여러개의 파라미터를 다른 html page로 이동할때 location.href = "(example?parameter1/parameter2)" 에서 GET방식의 특징을 이용하면 해결 가능하였다.
  • 해결 코드
  function reviews(title,author,publisher,publisher_year){
              console.log(title,author,publisher,publisher_year)
              location.href="reviews?" + title + "/" + author + "/" + publisher+"/"+publisher_year
  }

    <button class = "my-transparent-button" onclick="reviews('${title}','${author}')">

참고자료

(python flask) html으로 데이터 전달
js 를 이용한 html 변수값 주고 받기
url decoding
회원가입 폼 css


아쉬운 점

1.로그인 기능에서 우리는 token을 생성하여 서버에서 'exp' 변수에 만료시간을 넣어주어 시크릿키로 토큰을 풀 때 만료 되는 기능을 구현 하지는 못했다.

2.메인 페이지에서 보여지는 책에서 이미지가 없는 것을 볼 수 있는데 처음 사용하고자 하는 Open API에서 이미지 데이터가 있었지만 데이터의 형식이 xml이였고 이것을 Json 형식으로 바꾸는 과정에서 어려움이 있어 Json 형식을 가진 Open API를 찾게 된 것이다. 때문에 시각적으로 불편함이 있다.

3.프로젝트 시작에 있어 전체적인 css틀을 기준으로 코드 작성을 한 것이 아니라 각각의 기능들마다 css 부분이 통일되지 않아 보이는 이미지들이 부자연스러웠고 다음 프로젝트에는 부트스트랩과 같은 플랫폼을 이용하면 좋을듯 하다.

profile
이유를 찾아보자

1개의 댓글

comment-user-thumbnail
2023년 8월 11일

많은 도움이 되었습니다, 감사합니다.

답글 달기
Powered by GraphCDN, the GraphQL CDN