좋아요 기능을 구현하였습니다.
좋아요 클릭을 하는 부분이 두 곳이다.
1. 메인 페이지 카드 하단 부
2. 모달 페이지 하단 부
완료 작품을 아래에서 확인할 수 있습니다.
Database
구현을 하기에 앞서 database에 어떤 내용이 들어가야 할지 생각을 해 보았다.
1. post_id: 어떤 게시글에 좋아요가 붙어 있는지 알 수 있어야 하기 때문에 게시글의 고유 id가 필요하다. (게시글을 저장한 db에서 _id로 저장되어 있는 부분이다.)
2. user_id: 좋아요를 누른 사람의 id를 저장해 놓았다.
3. type: 좋아요(heart) 라고 저장을 하였다. 추후 좋아요 뿐만 아니라, 엄지를 들고 있는 부분 등 추가적으로 확장 가능성이 열려 있기 때문에 type을 따로 지정하여 저장하였다.
1번, 2번은 db에 필수적으로 저장해야 하는 목록이다.
3번 같은 경우는 추후 확장성을 고려한 부분이다.
로직
그렇다면 좋아요는 어떻게 구현을 할 것인가
app.py에서 구현할 부분은 2가지이다.
1. 사용자가 mainpage에 접근한 경우
- 게시글 db 정보 확인
- "좋아요" db 정보 확인
- 두 db의 정보를 합치고 이를 이용하여 모든 게시글의 좋아요 개수를 반영
- 모든 게시글 마다 로그인한 유저가 좋아요 버튼을 누른 여부를 확인하고 결과 반영
2. 사용자가 좋아요를 누른 경우
- 특정 게시글에 좋아요 상태를 확인 후 반대되는 상태를 반영
- db 데이터 삭제 또는 추가
1. 사용자가 mainpage에 접근했을 때
사용자가 mainpage에 접근을 할 때 GET 요청을 받을 것이고,
1. 게시글 마다 "좋아요"의 개수
2. 해당 게시글의 사용자가 좋아요를 눌렀는지 유무
2개의 결괏값을 프론트로 전달을 해주어야 한다.
우선 사용자가 mainpage에 접근을 했을 때 토큰에서 사용자의 id를 받아온다.
토큰에서 payload 부분에는 사용자의 id를 저장해 놓았으며
encode 했을 때와 반대로 decode를 이용하여 payload에서 사용자의 id를 받아올 수 있다.
token_receive = request.cookies.get('mytoken')
# 로그인한 토큰이 있는 경우(로그인 완료한 유저)
if token_receive != None:
try:
# 토큰으로 부터 payload를 불러옴
payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
# 페이로드 내에 있는 userid를 변수에 할당
userid = payload["id"]
이후 좋아요를 저장한 database에서 모든 정보를 받아온다.
"좋아요"에 대한 정보는 2개의 dictionary에 나눠서 저장을 하였다.
like_count : 특정 게시글의 좋아요의 개수
like_by_me: 특정 게시글에 사용자가 좋아요를 눌렀는지 유무
# 좋아요를 저장한 database에서 모든 정보를 받아옴
all_likes = db.likes.find({})
like_count = {}
like_by_me = {}
for like in all_likes:
# 좋아요를 누른 사람의 user id
like_user_id = like["username"]
# 현재 로그인한 사용자와, 게시글에 좋아요를 누른 사람이 동일하다면 해당 내역을 저장
if userid == like_user_id:
like_by_me[like["post_id"]] = True
# 게시글 마다 좋아요의 수를 저장
try:
like_count[like["post_id"]] += 1
except:
like_count[like["post_id"]] = 1
좋아요에 대한 정보는 정리를 완료하였다.
이번에는 게시글에 대한 정보를 정리해 볼 차례이다.
모든 게시글은 최신순으로 받아와 변수에 저장한다.
docs의 형태는 리스트이다.
리스트 안에는 게시글이 dictionary 형태로 저장되어 있을 것이다.
게시글 list를 for문으로 뽑아내고
각각의 게시글마다 좋아요 정보를 저장해 주었다.
저장한 정보는 front로 전달해주자.
# 보여줄 정보 전부 끌어오기(최신순,내림차순)
docs = list(db.test1.find({}).sort('_id', pymongo.DESCENDING))
for doc in docs:
doc["_id"] = str(doc["_id"])
# 현재 해당글의 좋아요 수가 몇개인지 적어라
doc["count_heart"] = like_count[doc["_id"]] if doc["_id"] in like_count else 0
# 내가 좋아요를 누른지의 유무
doc["heart_by_me"] = True if doc["_id"] in like_by_me else False
return render_template("mainpage.html", docs=docs, userid=userid) #메인페이지로 이동
mainpage get 요청에 대한 app.py의 전체 코드는 아래와 같다.
@app.route('/', methods=['GET'])
def home_get():
token_receive = request.cookies.get('mytoken')
if token_receive != None: # 로그인한 토큰이 있는 경우(로그인 완료한 유저)
try:
payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
userid = payload["id"]
print(userid)
# user_info = db.test1.find({"userid": userid})
all_likes = db.likes.find({})
like_count = {}
like_by_me = {}
for like in all_likes:
like_user_id = like["username"]
if userid == like_user_id:
like_by_me[like["post_id"]] = True
try:
like_count[like["post_id"]] += 1
except:
like_count[like["post_id"]] = 1
docs=list(db.test1.find({}).sort('_id', pymongo.DESCENDING)) #보여줄 정보 전부 끌어오기(최신순,내림차순)
for doc in docs:
doc["_id"] = str(doc["_id"])
# 현재 해당글의 좋아요 수가 몇개인지 적어라
doc["count_heart"] = like_count[doc["_id"]] if doc["_id"] in like_count else 0
# 내가 좋아요를 누른지의 유무
doc["heart_by_me"] = True if doc["_id"] in like_by_me else False
# for doc in docs:
# doc_id = doc["_id"]
# doc["_id"] = str(doc["_id"])
# # 현재 해당글의 좋아요 수가 몇개인지 적어라
# doc["count_heart"] = db.likes.count_documents({"post_id": doc["_id"], "type": "heart"})
# # 내가 좋아요를 누른지의 유무
# doc["heart_by_me"] = bool(db.likes.find_one({"post_id": doc["_id"], "type": "heart", "username": userid}))
return render_template("mainpage.html", docs=docs, userid=userid) #메인페이지로 이동
except jwt.ExpiredSignatureError:
return redirect(url_for("login", msg="로그인 시간이 만료되었습니다."))
except jwt.exceptions.DecodeError:
return redirect(url_for("login", msg="로그인 정보가 존재하지 않습니다."))
front로 전달을 해줄 때 모든 게시글 (docs)에 대한 정보와 사용자 아이디 (userid)에 대한 정보를 front로 전달을 해 주었다.
이것을 jinja2 문법을 이용하여 완성해보자.
우선 우리는 하트 이모티콘을 사용하기 위해 아래의 스크립트를 head 부분에 저장해 두자.
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
모달에 대한 하트 구현 부분도 카드와 동일하기 때문에
여기에서는 카드에 좋아요를 누르는 기능만 설명하도록 하겠다.
아래는 우리 팀이 구현한 카드이다.
bootstrap을 이용하여 구현한 듯합니다.
우리는 이 카드에 좋아요를 누르는 기능을 구현해볼 것이다.
<div class="col" role="button" data-bs-toggle="modal" data-bs-target="#exampleModal{{ loop.index }}">
<div class="card h-100">
<div class="card-header">{{doc.area}}</div>
<img src="../static/{{doc.img}}" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">{{doc.userid}}</h5>
<div class="content">
<span class="card-text">{{doc.title}}</span>
</div>
</div>
</div>
</div>
"좋아요"에는 어떤 기능이 들어가 있어야 할까
1. 게시글에 따라 좋아요 (하트)의 색상이 변해야 한다.
이전에 app.py에서 우리는 모든 게시글에 대한 정보를 front로 반환을 하였으며
게시글마다 좋아요의 개수와 유저의 좋아요를 눌렀는지 유무에 대한 결과 값이 저장되어 있다.
이를 이용하여 좋아요(하트)의 색상이 변한 상태로 페이지가 로딩될 수 있도록 구현하여야 한다.
jinja2 문법을 이용하여 구현을 해보자
fa-heart와 fa-heart-o는 각각 속이 꽉 찬 하트와 속이 빈 하트를 의미한다.
jinaja2에서 제공하는 if else문을 이용하여
과거에 사용자가 특정 게시글에 좋아요를 눌렀는 경우에는 fa-heart가 들어갈 수 있도록,
과거에 사용자가 특정 게시글에 좋아요를 누르지 않은 경우에는 fa-heart-o가 들어갈 수 있도록 구현하였다.
현재 사용자가 게시글을 누를 경우에는 toggle_like 함수가 실행될 수 있도록 한다.
<nav>
<div>
<!-- 좋아요를 누르면 toggle_like 함수가 실행된다. -->
<!-- toggle like 함수는 게시글의 id와, 어떤 이모티콘을 눌렀는지 type에 대한 정보를 인자로 제공한다. -->
<a class="level-item is-sparta" aria-label="heart-card" onclick="toggle_like('{{doc._id}}', 'heart')">
<!-- 사용자가 좋아요를 누른 경우 -->
{% if doc.heart_by_me == True %}
<span class="icon is-small"><i class="fa fa-heart"aria-hidden="true"></i></span>
<span class="like-num">{{doc.count_heart}}</span>
<!-- 사용자가 좋아요를 누르지 않은 경우 -->
{% else %}
<span class="icon is-small"><i class="fa fa-heart-o" aria-hidden="true"></i></span>
<span class="like-num">{{doc.count_heart}}</span>
{% endif %}
</a>
</div>
</nav>
게시글을 누른 이후에는 어떤 기능이 필요할까?
1. 사용자가 누른 게시글의 좋아요 상태를 변경해 주어야 한다.
- 현재 좋아요가 눌러져 있는 상태 (꽉 찬 하트) 라면 db에서 좋아요 정보를 지워야 하고
- 현재 좋아요가 눌러져 있지 않은 상태 (빈 하트) 라면 db에 좋아요 정보를 추가해 주어야 한다.
이를 구현하기 위해서는 우선 현재 게시글의 좋아요의 하트가 빈 하트인지 꽉 찬 하트인지를 알 수 있어야 한다.
좋아요의 상태는 어떻게 알 수 있을 까?
Jquery 요소 찾기를 이용하여 i 태그에 fa-heart가 들어있는지 fa-heart-o 가 들어가 있는지를 확인한다.
Jquery 요소 찾기는 아래 블로그에 잘 정리되어 있다.
https://cornswrold.tistory.com/322
위의 navy class를 카드에 삽입해준다.
아래 코드가 카드 내에 좋아요 기능을 삽입한 html이다.
<div class="col {{ doc._id }}_card" role="button" data-bs-toggle="modal" data-bs-target="#exampleModal{{ loop.index }}">
<div class="card h-100">
<div class="card-header">{{doc.area}}</div>
<img src="../static/{{doc.img}}" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">{{doc.userid}}</h5>
<div class="content">
<span class="card-text">{{doc.title}}</span>
<nav>
<div>
<!-- 좋아요를 누르면 toggle_like 함수가 실행된다. -->
<!-- toggle like 함수는 게시글의 id와, 어떤 이모티콘을 눌렀는지 type에 대한 정보를 인자로 제공한다. -->
<a class="level-item is-sparta" aria-label="heart-card" onclick="toggle_like('{{doc._id}}', 'heart')">
<!-- 사용자가 좋아요를 누른 경우 -->
{% if doc.heart_by_me == True %}
<span class="icon is-small"><i class="fa fa-heart"aria-hidden="true"></i></span>
<span class="like-num">{{doc.count_heart}}</span>
<!-- 사용자가 좋아요를 누르지 않은 경우 -->
{% else %}
<span class="icon is-small"><i class="fa fa-heart-o" aria-hidden="true"></i></span>
<span class="like-num">{{doc.count_heart}}</span>
{% endif %}
</a>
</div>
</nav>
</div>
</div>
</div>
</div>
fa-heart와, fa-heart-o를 찾아보자
찾는 방법은 아래와 같다.
1. 클래스의 이름이 "{{ doc._id }}_card" 인 것을 찾고
2. 하위 클래스에서 a 태그에 aria-label="heart-card" 인 부분을 찾는다.
3. 그 하위 클래스에 있는 i 태그를 찾는다.
위 3가지 내용을 코드로 나타내면 아래와 같다.
let $a_like_card = $(`.${post_id}_card a[aria-label='heart-card']`)
let $i_like_card = $a_like_card.find("i")
위에서 찾은 i 태그에서 fa-heart 클래스가 있을 경우와 fa-heart-o가 있을 경우를 나눠서 POST 요청을 전송한다.
if ($i_like.hasClass("fa-heart")) {
$.ajax({
type: "POST",
url: "/update_like",
data: {
post_id_give: post_id,
type_give: type,
action_give: "unlike"
},
success: function (response) {
}
})
} else {
$.ajax({
type: "POST",
url: "/update_like",
data: {
post_id_give: post_id,
type_give: type,
action_give: "like"
},
success: function (response) {
}
})
}
}
이제 마지막이다.
post 요청을 처리해보자.
어떻게 처리하면 될까?
1. 좋아요가 눌러져 있는 상태라면 database에서 좋아요 정보를 지운다.
2. 좋아요가 눌러져 있지 않은 상태라면 database에서 좋아요 정보를 추가한다.
@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'])
# 현재 로그인한 유저 id
user_info = db.test1User.find_one({"userid": payload["id"]})
# 게시글의 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["userid"],
"type": type_receive
}
# 좋아요가 눌러져 있지 않는 상태라면 (db에서 좋아요 정보를 넣는다.)
if action_receive == "like":
db.likes.insert_one(doc)
# 좋아요가 눌러져 있는 상태라면 (db에서 좋아요 정보를 지운다.)
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"))