2021년 5월 29일 개발일지

지난 개발일지까지 로그인 기능에 대해 살펴보았다.

회원가입 기능 V
로그인 기능 V
회원 포스팅 기능
좋아요 기능
프로필 페이지 수정 기능

이제 회원이 로그인을 했을 때에 보이는 화면을 구성하고, 회원 포스팅 기능을 완성해 보자.

로그인을 하면 포스팅을 올릴 수 있고, 다음과 같이 메인 화면이 서버에 저장된 포스팅들이 최대 20개까지 쭉 뜨게 만들 것이다.

메인의 포스팅 칸과, 포스팅 칸을 클릭하면 열리는 포스팅 모달을 만들어 주자.

(1) 포스팅 칸

>  <article class="media">
        <figure class="media-left" style="align-self: center">
            <a class="image is-32x32" href="/user/{{ user_info.username }}">
                <img class="is-rounded" src="{{ url_for('static', filename=user_info.profile_pic_real) }}">
            </a>
        </figure>
        <div class="media-content">
            <div class="field">
                <p class="control">
                    <input id="input-post" class="input is-rounded" placeholder="Share your story"
                           onclick='$("#modal-post").addClass("is-active")'></p>
            </div>
        </div>
    </article>

id가 input-post인 포스팅하기를 누르면, is-active Class가 추가된다.

아래 사진에서 노란색 부분이 포스팅 칸인데, 앞의 이미지를 프로필 사진으로 넣어주기 위해서는 서버에서 사용자 정보를 같이 보내주어야 한다. (로그인 기능 살펴보며 이미 살펴본 코드다.)

@app.route('/')
def home():
    token_receive = request.cookies.get('mytoken')
    try:
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
        user_info = db.users.find_one({"username": payload["id"]})
        return render_template('index.html', user_info=user_info)
    except jwt.ExpiredSignatureError:
        return redirect(url_for("login", msg="로그인 시간이 만료되었습니다."))
    except jwt.exceptions.DecodeError:
        return redirect(url_for("login", msg="로그인 정보가 존재하지 않습니다."))

위 사진에서 Share your story를 누르면 다음과 같이 포스팅 모달이 활성화된다. (Class에 is-active가 추가됨.)

이것도 만들어 주도록 하자.

<div class="modal" id="modal-post">
    <div class="modal-background" onclick='$("#modal-post").removeClass("is-active")'></div>
    <div class="modal-content">
        <div class="box">
            <article class="media">
                <div class="media-content">
                    <div class="field">
                        <p class="control">
                                    <textarea id="textarea-post" class="textarea"
                                              placeholder="Share your story"></textarea>
                        </p>
                    </div>
                    <nav class="level is-mobile">
                        <div class="level-left">

                        </div>
                        <div class="level-right">
                            <div class="level-item">
                                <a class="button is-sparta" onclick="post()">posting</a>
                            </div>
                            <div class="level-item">
                                <a class="button is-sparta is-outlined"
                                   onclick='$("#modal-post").removeClass("is-active")'>cancel</a>
                            </div>
                        </div>
                    </nav>
                </div>
            </article>
        </div>
    </div>
    <button class="modal-close is-large" aria-label="close"
            onclick='$("#modal-post").removeClass("is-active")'></button>

위 코드에서 세 부분에서 다음 코드가 등장한다.

onclick = '$("#modal-post").removeClass("is-active")'

클릭하면 is-active class를 제거하는 함수이다.
어디에 활성화되어있는지 보면, "modal-background"를 클릭할 때, 상단의 X버튼을 클릭할 때, Close 버튼 클릭할 때이다.

이제 활성화된 포스팅 모달 박스에 텍스트를 입력해서 포스팅을 해보자.
클라이언트에서 글과 현재 시각을 문자열로 받아 POST 요청을 보내고, 저장에 성공하면 모달을 닫고 새로고침한다.

function post() {
    let comment = $("#textarea-post").val()
    let today = new Date().toISOString()

    if (comment == "") {
        alert('텍스트를 입력하세요.')
    } else {
        $.ajax({
            type: "POST",
            url: "/posting",
            data: {
                comment_give: comment,
                date_give: today
            },
            success: function (response) {
                $("#modal-post").removeClass("is-active")
                window.location.reload()
            }
        })
    }
}
     

서버에서는 글과 현재 시각을 받아 로그인한 사용자의 정보로부터 아이디, 이름, 프로필 사진을 같이 저장한다.

@app.route('/posting', methods=["POST"]
def posting():
    token_receive = request.cookies.get('mytoken')
    try:
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
        user_info = db.users.find_one({"username": payload["id"]})
        comment_receive = request.form["comment_give"]
        date_receive = request.form["date_give"]
        doc = {
            "username": user_info["username"],
            "profile_name": user_info["profile_name"],
            "profile_pic_real": user_info["profile_pic_real"],
            "comment": comment_receive,
            "date": date_receive
        }
        db.users.insert_one(doc)
        return jsonify({"result": "success", 'msg': '포스팅 성공'})
   except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
        return redirect(url_for("home"))

이제 포스팅을 했으니, 저장한 포스트들을 받아와서 화면에 보여주도록 하자.

여기서 잠깐1!) 아까 post할 때 date를 받아왔다고 했는데, 이 date를 그냥 보여주게 되면 엄청 자세하게 분, 초까지 나온다. 따라서 이 date를 좀 덜 자세하게? 그리고 우리가 알고 싶은 형태로 바꿔주는 작업이 필요하다.

function time2str(date) {
   let today = new Date()
   let time = (today - date) / 1000 / 60 //분
   
   if (time < 60) {
       return parseInt(time) + "분 전"
   }
   
   time = time / 60 // 시간
   if (time < 24) {
       return parseInt(time) + "시간 전"
   }
   time = time / 24
   if (time < 7) {
       return parseInt(time) + "일 전"
   }
   return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`
}

위 코드를 통해서, 우리가 SNS에서 많이 보는 형태인 "분 전", "시간 전", "일 전"으로 Posting 시점을 보여줄 수 있다.

여기서 잠깐!2) Post들을 보여줄 때, 좋아요 수도 같이 보여줄 것이다. 근데 이것도 숫자 그대로 막 1의자리 까지 보여주기 싫으니, 천의 자리로 보여줄 것이다.

function num2str(count) {
    if (count > 10000) {
        return parseInt(count / 1000) + "k"
    }
    if (count > 500) {
        return parseInt(count / 100) / 10 + "k"
    }
    if (count == 0) {
        return ""
    }
    return count
}

이렇게 작업을 해 준 후 get_post 함수를 사용한다.
서버에서 받아온 포스트들을 클라이언트에게 다음과 같이 보여줄 것이다.

function get_posts(username) {
    if (username == undefined) {
        username = ""
    }
    $("#post-box").empty()
    $.ajax({
        type: "GET",
        url: `/get_posts?username_give=${username}`,
        data: {},
        success: function (response) {
            if (response["result"] == "success") {
                let posts = response["posts"]
                for (let i = 0; i < posts.length; i++) {
                    let post = posts[i]
                    let time_post = new Date(post["date"])
                    let time_before = time2str(time_post)
                    let class_heart = post['heart_by_me'] ? "fa-heart" : "fa-heart-o"
                    let count_heart = post['count_heart']
                    let class_star = post['star_by_me'] ? "fa-star" : "fa-star-o"
                    let count_star = post['count_star']
                    let class_thumbs = post['thumbs_by_me'] ? "fa-thumbs-up" : "fa-thumbs-o-up"
                    let count_thumbs = post['count_thumbs']
                    let html_temp = `<div class="box" id="${post["_id"]}">
                                        <article class="media">
                                            <div class="media-left">
                                                <a class="image is-64x64" href="/user/${post['username']}">
                                                    <img class="is-rounded" src="/static/${post['profile_pic_real']}"
                                                         alt="Image">
                                                </a>
                                            </div>
                                            <div class="media-content">
                                                <div class="content">
                                                    <p>
                                                        <strong>${post['profile_name']}</strong> <small>@${post['username']}</small> <small>${time_before}</small>
                                                        <br>
                                                        ${post['comment']}
                                                    </p>
                                                </div>
                                                <nav class="level is-mobile">
                                                    <div class="level-left">
                                                        <a class="level-item is-sparta" aria-label="heart" onclick="toggle_like('${post['_id']}', 'heart')">
                                                            <span class="icon is-small"><i class="fa ${class_heart}"
                                                                                           aria-hidden="true"></i></span>&nbsp;<span class="like-num">${count_heart}</span>
                                                        </a>
                                                        <a class="level-item is-sparta" aria-label="star" onclick="toggle_like('${post['_id']}', 'star')">
                                                            <span class="icon is-small"><i class="fa ${class_star}"
                                                                                           aria-hidden="true"></i></span>&nbsp;<span class="like-num">${count_star}</span>
                                                        </a>
                                                        <a class="level-item is-sparta" aria-label="thumbs" onclick="toggle_like('${post['_id']}', 'thumbs')">
                                                            <span class="icon is-small"><i class="fa ${class_thumbs}"
                                                                                           aria-hidden="true"></i></span>&nbsp;<span class="like-num">${count_thumbs}</span>
                                                        </a>
                                                    </div>

                                                </nav>
                                            </div>
                                        </article>
                                    </div>`
                    $("#post-box").append(html_temp)
                }
            }
        }
    })
}                   

get_posts 함수는 화면이 로딩되자마자 수행되도록 한다.

$(document).ready(function(){
   get_posts()
})   

서버에서는 다음과 같은 함수로 포스트를 가져온다.

@app.route("/get_posts", methods=['GET'])
def get_posts():
    token_receive = request.cookies.get('mytoken')
    try:
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])

        username_receive = request.args.get("username_give")
        if username_receive == "":
            posts = list(db.posts.find({}).sort("date", -1).limit(20))
        else:
            posts = list(db.posts.find({"username": username_receive}).sort("date", -1).limit(20))

        for post in posts:
            post["_id"] = str(post["_id"])
            post["count_heart"] = db.likes.count_documents({"post_id": post["_id"], "type": "heart"})
            post["heart_by_me"] = bool(
                db.likes.find_one({"post_id": post["_id"], "type": "heart", "username": payload['id']}))

            post["count_star"] = db.likes.count_documents({"post_id": post["_id"], "type": "star"})
            post["star_by_me"] = bool(
                db.likes.find_one({"post_id": post["_id"], "type": "star", "username": payload['id']}))

            post["count_thumbs"] = db.likes.count_documents({"post_id": post["_id"], "type": "thumbs"})
            post["thumbs_by_me"] = bool(
                db.likes.find_one({"post_id": post["_id"], "type": "thumbs", "username": payload['id']}))
        return jsonify({"result": "success", "msg": "포스팅을 가져왔습니다.", "posts": posts})

    except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
        return redirect(url_for("home"))

여기까지 해서 포스팅하기, 포스팅된 것들을 메인 화면에 보여주기 작업까지 진행했다.

회원들이 포스팅한 것들은 robo3T에서 다음과 같은 db로 확인 가능하다.

회원가입 기능 V
로그인 기능 V
회원 포스팅 기능 V
좋아요 기능
프로필 페이지 수정 기능

이제 좋아요 기능을 만들어 보자.
위에서 코드를 살펴보면 알 수 있을 텐데, 세 가지의 좋아요가 있다.
1) heart 2) star 3) thumbs-up

그럼 좋아요 함수를 종류대로 써야 할까?
첫 시간에 배운 toggle 함수를 통해 한 번에 세 가지 종류의 좋아요 기능을 구현할 수 있다.

세 가지 좋아요를 like할 수 있는 type로 구분하고, 좋아요 된 상태의 아이콘은 class_s로, 아닌 상태는 class_o로 구분한다.

type별로 class_s인 상태라면 아이콘을 클릭해 함수가 실행되는 경우 unlike라는 action을 서버로 보내게 되고, class_o로 상태가 바뀐다.
한 편, class_o였던 상태라면, 아이콘을 클릭해 함수가 실행되는 경우 like라는 action을 서버로 보내게 되고, class_s로 상태가 바뀐다.

class가 바뀔때 아이콘 모양도 바뀐다. 어떻게? class_s일 때는 하트, 별, 엄지손가락 모두 색이 채워지나, class_o인 경우는 색이 빈 상태의 아이콘이 나온다.

여기서 중요한 것은, post를 구분해 주지 않으면, 모든 post에 대해 일괄적인 좋아요가 적용되기 때문에, post들을 post[i]로 보고 구별해서 각각에 post에 대해 좋아요 기능을 주어야 한다.

function toggle_like(post_id, type) {
    console.log(post_id, type)
    let $a_like = $(`#{post_id} a[aria-label='${type}']`)
    let $i_like = $a_like.find("i")
    let class_s = {"heart" : "fa-heart","star" : "fa-star", "thumbs" : "fa-thumbs-up"}
    let class_o = {"heart" : "fa-heart-o","star" : "fa-star-o", "thumbs" : "fa-thumbs-o-up"}
    if ($i_like.hasClass(class_s[type])) {
        $.ajax({
           type: "POST",
           url: "/update_like",
           data: {
               post_id_give: post_id,
               type_give: type,
               action_give: "unlike"
          },
          success: function(response) {
              console.log("unlike")
              $i_like.addClass(class_o[type]).removeClass(class_s[type])
              $a_like.find("span.like_num").text(num2str(response)["count"]))
         }
      })
   } else {
      $.ajax({
           type: "POST",
           url: "/update_like",
           data: {
                post_id_give: post_id,
                type_give: type,
                action_gie: "like",
           },
           success: function (response) {
                console.log("like")
                $i_like.addClass(class_s[type]).removeClass(class_o[type])
                $a_like.find("span.like-num").text(response["count"])
           }
       })
     }
   }  
       
    

서버에서는 이 함수를 어떻게 받아올까?
일단 회원임을 입증하는 token을 받아서, user가 맞는지 확인한다. 그 후에 프론트에서 post id와 action을 준 username, 그리고 좋아요 type (star인지, heart인지, thumbs인지)를 받아서 DB에 저장한다.

action 종류가 like이면 db에 저장하고, unlike이면 db 삭제한다.
post별로 좋아요 수도 count해서 get post 함수 요청 시 보여줄 준비를 한다.

@app.route('/update_like', methods=['POST'])
def update_like():
    token_receive = request.cookies.get('mytoken')
    try:
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
        user_info = db.users.find_one({"username": payload["id"]})
        post_id_receive = request.form["post_id_give"]
        type_receive = request.form["type_give"]
        action_receive = request.form["action_give"]
        doc = {
           "post_id": post_id_receive,
           "username": user_info["username"],
           "type": type_receive
        }
        if action_receive == "like":
            db.likes.insert_one(doc)
        else:
            db.likes.delete_one(doc)
        count = db.likes.count_documents({"post_id": post_id_receive, "type": type_receive})
        return jsonify({"result": "success", 'msg': 'updated', "count":count})
except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
       return redirect(url_for("home"))

이렇게 구현된 좋아요 함수의 결과는 get_post 함수를 통해 프론트엔드에 보여지게 된다.

Robo3T에서 보면 db를 다음과 같은 모습으로 확인할 수 있다.

여기까지 좋아요 기능까지 살펴보았다.(고지가 멀지 않았다...)

회원가입 기능 V
로그인 기능 V
회원 포스팅 기능 V
좋아요 기능 V
프로필 페이지 수정 기능

그럼 마지막으로 프로필 페이지 수정 기능까지 진행해보자

위와 같이 프로필 화면에 접속하면 내 프로필 사진, 소개, 내가 포스팅 한 글만 모아서 볼 수 있다.
그리고 프로필을 수정할 수 있으며, 로그아웃을 여기서 진행하면 된다.

그리고 프로필 수정을 클릭하면 포스팅 때와 비슷하게 프로필 수정 모달이 뜨게 된다.

javascript를 통해 프로필 화면을 띄웠을 때 필요한 get_posts 함수, 로그아웃 함수, 프로필 수정 함수를 차례대로 작성해 보자.

// get_posts 함수, 로그인한 user가 포스팅한 포스트만 모아서 볼 수 있다.
$(document).ready(function() {
    get_posts('{{user_info.username}}');
})

// 로그아웃 하기
function sign_out() {
    $.removeCookie('mytoken', {path: '/'});
    alert('로그아웃!')
    window.location.href = "/login"
}   

// 프로필 업데이트 하기
function update_profile() {
    let name = $('#input-name').val()
    let file = $('#input-pic')[0].files[0]
    let about = $("#textarea-about").val()
    let form_data = new Formdata()
    form_data.append("file_give", file)
    form_data.append("name_give", name)
    form_data.append("about_give", about)
    console.log(name, file, about, form_data)
    
    $.ajax({
        type: "POST",
        url: "/updata_profile",
        data: form_data,
        cache: false,
        contentType: false,
        processData: false,
        success: function(response) {
            if (response["result"] == "success") {
                alert(response["msg"])
                window.location.reload()
            }
          }
       });
    }     

각 사용자의 프로필과 글을 모아볼 수 있는 공간에 필요한 서버 코드는 다음과 같다.

@app.route('/user/<username>')
def user(username):
   token_receive = request.cookies.get('mytoken')
   try:
      payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
      status = (username == payload["id"])
      user_info = db.users.find_one({"username": username}, {"_id": False})
      return render_template('user.html', user_info=user_info, status=status)
except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
      return redirect(url_for("home"))    

프로필 이름, 프로필 정보, 프로필 사진을 저장하고 받아오는 서버 함수는 다음과 같다.
여기서 좋은 건 회원들이 사진을 올리면 그 사진이 내가 정한 이름으로 내가 정한 폴더에 저장된다는 것이다..(업무에서 잘 활용해보고 싶은 마음,,)

@app.route('/update_profile', methods=['POST'])
def save_img():
    token_receive = request.cookies.get('mytoken')
    try:
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
        username = payload["id"]
        name_receive = request.form["name_give"]
        about_receive = request.form["about_give"]
        new_doc = {
            "profile_name": name_receive,
            "profile_info": about_receive
        }
        if 'file_give' in request.files:
            file = request.files["file_give"]
            filename = secure_filename(file.filename)
            extension = filename.split(".")[-1]
            file_path = f"profile_pics/{username}.{extension}"
            file.save("./static/" + file_path)
            new_doc["profile_pic"] = filename
            new_doc["profile_pic_real"] = file_path
        db.users.update_one({'username': payload['id']}, {'$set': new_doc})
        return jsonify({"result": "success", 'msg': '프로필을 업데이트했습니다.'})
    except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
        return redirect(url_for("home"))

이렇게 해서 내가 db에 저장한 user 정보는 다음과 같이 robo 3T에서 확인할 수 있다. (회원가입 시간에도 소개하였다.)

여기까지 해서 회원가입 기능부터 프로필 페이지에서 수정하기 기능까지 모두 살펴보았다. 이걸로 웹개발 플러스 4주차 강의가 마무리 되었다.

회원가입 기능 V
로그인 기능 V
회원 포스팅 기능 V
좋아요 기능 V
프로필 페이지 수정 기능 V

튜터님과 나자신 수고했다. 그럼 이만

좋은 웹페이지 즐겨찾기