좋은 숙박 시설을 찾아보자! 좋은 여행 숙박 시설을 찾고 싶을 때 사용해봐요!
항해 99 1주차 프로젝트
22.05.09(월) ~ 22.05.12(목)
조장 이은총
Role 로그인/회원가입 페이지 및 세부 기능 업데이트
조원 박민수
Role 호텔등록 메인페이지 UI 및 서버api 제작
조원 박세열
Role 세부 리뷰 페이지 총괄 및 데이터 베이스 관리
자신이 갔다온 호텔을 추가하여 리뷰를 남길 수 있는 사이트입니다.
S.A. Link:https://velog.io/@tlfqjrjs1234/Chapter11%EC%A1%B0-S.AStarting-Assignment
Youtube Link: https://youtu.be/gIOlvXtVCNE/
- 운영체제 : Windows, mac
- 개발 도구 : pycharm, visual studio code
- 개발 언어 : html, python
- 데이터베이스 : MongoDB
- 회원가입/로그인을 통한 계정 정보 저장 및 관리
- 회원가입 후 로그인으로 메인페이지에서 호텔 설명 카드 열람
- 상세 리뷰 버튼으로 상세 리뷰 페이지 이동하여 호텔에 대한 여러 코멘트들 열람 가능
- 마음에 드는 코멘트에 좋아요를 표시하여 좋아요 집계 가능
- 마음에 안드는 호텔 카드 및 코멘트 삭제 가능
- 로그인으로 받은 토큰이 없다면 로그인 페이지 이외의 타 페이지 강제 이동 불가능
호텔의 전체적인 데이터들을 저장하는 데이터베이스
- hotel_id
호텔이 생성될 때 할당 받는 아이디 - name
호텔 이름 - hotel rate
호텔이 몇성급인지 보여주는 인스턴스 - hotel address
호텔 주소
회원가입한 사용자의 데이터들을 저장하는 데이터베이스
- user id
사용자의 아이디 - password
사용자의 패스워드 - nickname
사용자의 닉네임
호텔의 id와 유저의 id를 이용해 누가 어느 호텔에 코멘트를 달았는지 볼 수 있는 데이터베이스
- comment id
코멘트가 생성될 때 자체적으로 생기는 아이디 - comment rate
사용자가 코멘트를 작성할 때 매기는 별점 - nickname
사용자의 닉네임 - hotel_id
호텔이 생성될 때 할당 받는 아이디
누가 무슨 코멘트를 좋아요를 했는지 알 수 있는 데이터베이스
- likes id
좋아요가 생성될 때 자체적으로 생기는 아이디 - comment id
코멘트가 생성될 때 자체적으로 생기는 아이디 - nickname
사용자의 닉네임
서버측 코드
@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({"user_id": payload["user_id"]})
return redirect(url_for("info"))
except jwt.ExpiredSignatureError:
return redirect(url_for("login"))
except jwt.exceptions.DecodeError:
return redirect(url_for("login"))
- 토큰이 남아 있는지 확인하여 토큰이 있으면 메인페이지로 아닐 경우에는 로그인 페이지로 보내준다
- rediret(url_for("(함수 명)") 함수명에 해당되는 파라미터로 보냄
@app.route('/sign_in', methods=['POST'])
def sign_in():
# 로그인
user_id_receive = request.form['user_id_give']
password_receive = request.form['password_give']
pw_hash = hashlib.sha256(password_receive.encode('utf-8')).hexdigest()
result = db.users.find_one({'user_id': user_id_receive, 'password': pw_hash})
if result is not None:
payload = {
'user_id': user_id_receive,
'exp': datetime.utcnow() + timedelta(seconds=60 * 60 * 1) # 로그인 1시간 유지
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
# .decode('utf-8')
print(token)
return jsonify({'result': 'success', 'token': token})
else:
return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})
- 로그인 api이며 입력한 아이디와 암호화된 비밀번호를 통해 find하여 none이 아니면 유저아이디와 로그인 시간을 가지고 있는 토큰을 쿠키에 넣는다
- hashlib.sha256(명.encode('utf-8')).hexdigest() 암호화 시킨다. exp는 지속 시간을 나타낸다
@app.route('/sign_up/save', methods=['POST'])
def sign_up():
user_id_receive = request.form['user_id_give']
password_receive = request.form['password_give']
nickname_receive = request.form['nickname_give']
print(user_id_receive,password_receive,nickname_receive)
password_hash = hashlib.sha256(password_receive.encode('utf-8')).hexdigest()
doc = {
"user_id": user_id_receive, # 아이디
"password": password_hash, # 비밀번호
"nickname": nickname_receive # 닉네임
}
db.users.insert_one(doc)
return jsonify({'result': 'success'})
- 로그인 비밀번호 닉네임을 확인하여 데이터베이스에 추가
@app.route('/sign_up/check_id_dup', methods=['POST'])
def check_id_dup():
user_id_receive = request.form['user_id_give']
print(user_id_receive)
exists = bool(db.users.find_one({"user_id": user_id_receive}))
print(exists)
return jsonify({'result': 'success', 'exists': exists})
@app.route('/sign_up/check_nickname_dup', methods=['POST'])
def check_nickname_dup():
nickname_receive = request.form['nickname_give']
exists = bool(db.users.find_one({"nickname": nickname_receive}))
return jsonify({'result': 'success', 'exists': exists})
- 로그인과 닉네임이 같은 사용자가 있는 지 없는 지 확인하는 코드
- bool()은 true/false로 변환한다
$.cookie("mytoken", response["token"], { path: "/" });
$.removeCookie('mytoken', {path: '/'});
- $.cookie는 path에 있는 파라미터에 토큰을 준다 {path:"/"} 전역에 토큰을 부여
- $.removeCookie는 path에 있는 파라미터 토큰을 삭제 {path:"/" 전역 토큰 삭제
서버측 코드
@app.route('/main')
def info():
token_receive = request.cookies.get('mytoken')
try:
payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
hotel_list = list(db.hotel.find({}, {'_id': False}))
reviewer = payload["user_id"]
return render_template('main.html', rows=hotel_list, user_id=reviewer)
except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
return redirect(url_for("login"))
- 페이지로 들어 올 때 로그인유효성 검사를 위해 쿠키에서 토큰정보를 확인 후 통과시 등록되어있는 호텔데이터와 본인의 아이디정보를 가지고 메인페이지로보내주고 토큰정보가 없거나 유효하지않다면 로그인페이지로 돌아간다.
- 데이터베이스에 등록되어 있는 호텔정보를 Jinja2언어로 클라이언트에서 받기 위해 이동시에 파라메터로 보내준다.
- 클라이언트 측에서 작성자 본인이 아니면 삭제버튼이 안보이게 하기위해 본인의 아이디 정보를 파라메터로 보내준다.
@app.route("/info", methods=["POST"])
def hotel_post():
token_receive = request.cookies.get('mytoken')
try:
payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
hotel_list = list(db.hotel.find({}))
try:
if hotel_list[-1]["hotel_id"] is None:
count = 0
else:
count = hotel_list[-1]["hotel_id"] + 1
except(IndexError):
print("에러")
count = 0
reviewer = payload["user_id"]
hotel_image_receive = request.form['url_give']
hotel_rate_receive = request.form['star_give']
name_receive = request.form['title_give']
address_receive = request.form['hotel_address_give']
doc = {
'hotel_image':hotel_image_receive,
'hotel_rate':hotel_rate_receive,
'name':name_receive,
'address':address_receive,
'hotel_id': count,
'reviewer' : reviewer
}
db.hotel.insert_one(doc)
return jsonify({'msg':'등록 완료'})
except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
return redirect(url_for("login"))
- 호텔등록코드부분, 이전코드와 마찬가지로 토큰으로 유효성검사를하고 데이터베이스의 호텔id와 중복되지 않게 id를 할당하고 클라이언트 측에서 보내준 입력데이터를 포스팅한다.
@app.route("/info/delete", methods=["POST"])
def hotel_delete():
token_receive = request.cookies.get('mytoken')
try:
payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
reviewer = payload["user_id"]
hotel_id_receive = request.form['hotel_id_give']
card_info = db.hotel.find_one({'hotel_id':int(hotel_id_receive)})
card_reviewer = card_info['reviewer']
if reviewer == card_reviewer:
db.hotel.delete_one({'hotel_id':int(hotel_id_receive)})
return jsonify({'msg':'삭제 완료'})
else:
return jsonify({'msg':'작성자만 삭제가능합니다.'})
except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
return redirect(url_for("login"))
- 포스팅 삭제를 위한 서버코드, 마찬가지로 로그인이 되어있는지 검사하고 본인이 등록한 호텔데이터인지 확인후 삭제한다.
function to_review(hotel_id) {
let hotel_num = hotel_id;
window.location.href = "/reviews?num=" + hotel_num;
}
- 호텔카드마다 붙어있는 버튼을 누를 시 호텔아이디를 가지고 리뷰페이지로 넘어가기 위한 클라이언트 코드.
function posting() {
let url = $("#url").val();
let star = $("#star").val();
let title = $("#title").val();
let hotel_address = $("#address").val();
$.ajax({
type: "POST",
url: "/info",
data: {
url_give: url,
star_give: star,
title_give: title,
hotel_address_give: hotel_address,
},
success: function (response) {
alert(response["msg"]);
window.location.reload();
},
});
}
*호텔 등록창에 입력한 데이터를 포스트 요청ajax코드를 사용하여 서버측으로 넘겨준다.
function delete_review(hotel_id) {
$.ajax({
type: "POST",
url: "/info/delete",
data: {
hotel_id_give: hotel_id,
},
success: function (response) {
alert(response["msg"]);
window.location.reload();
},
});
}
- 삭제할 호텔아이디를 서버측으로 넘겨주는 포스트요청ajax코드이다.
{% for row in rows %} {% set hotel_image = row['hotel_image'] %}
{% set name = row['name'] %} {% set address = row['address'] %}
{% set hotel_rate = row['hotel_rate'] %}
{% set hotel_id = row['hotel_id'] %}
{% set reviewer = row['reviewer'] %}
{% set hotel_rate_image ="" %}
{% if hotel_rate == "1" %}
{% set hotel_rate_image = "⭐" %}
{% elif hotel_rate == "2" %}
{% set hotel_rate_image = "⭐⭐" %}
{% elif hotel_rate == "3" %}
{% set hotel_rate_image = "⭐⭐⭐" %}
{% elif hotel_rate == "4" %}
{% set hotel_rate_image = "⭐⭐⭐⭐" %}
{% elif hotel_rate == "5" %}
{% set hotel_rate_image = "⭐⭐⭐⭐⭐" %}
{% endif %}
<div class="col">
<div class="card h-100">
<img src="{{hotel_image}}" class="card-img-top" />
<div class="card-body">
<h5 class="card-title">{{name}}</h5>
<p class="card-text">{{address}}</p>
<p>{{hotel_rate_image}}</p>
<p class="comment">
<button onclick="to_review({{hotel_id}})">상세리뷰</button>
{% if reviewer == user_id %}
<button onclick="delete_review({{ hotel_id }})">삭제</button>
{% endif %}
</p>
</div>
</div>
</div>
{% endfor %}
- 서버측에서 넘겨준 파라메터 데이터를 Jinja2방식을 사용하여 포스팅카드로 나타내는 부분의 코드이다.
코드의 길이 관계로 백엔드 서버 쪽과 자바스크립트 함수 부분만 설명하겠다.
@app.route('/reviews')
def reviews():
token_receive = request.cookies.get('mytoken')
try:
payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
return render_template('reviews.html')
except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
return redirect(url_for("login"))
except KeyError:
return redirect(url_for("login"))
except UnboundLocalError:
return redirect(url_for("login"))
- 메인페이지에서 사용자가 토큰을 가지고 넘어오면 그것을 기반으로 "reviews.html"을 렌더해서 클라이언트한테 보내준다.
$(document).ready(function () {
let getLink = window.location.search;
let num = getLink.split("=")[1];
get_posts();
});
- 사용자는 토큰과 함께 메인페이지에서 num이라는 변수로 hotel_id를 가져온다. 이 hotel_id는 상세 리뷰에서 해당 호텔에 관련된 코멘트들만 불러모으기 위한 키 값으로 작용한다.
- 페이지 렌더시 기본적으로 작동이 되는 함수를 모아서 작동시킨다. 여기에 자바스크립트로 이루어진 get_posts함수를 담아 코멘트를 가지고 온다.
## get_posts.js 함수의 실행 전반부
function get_posts() {
let getLink = window.location.search;
let num = getLink.split("=")[1];
$.ajax({
type: "POST",
url: "/get_posts",
data: { hotel_id_give: num },//여기까지
- get_posts()함수에서 타입을 post로 data에 hotel_id를 담아 app.py 백엔드 서버에 보내준다. 이를 통해 벡엔드에서 해당 hotel_id와 연관이 있는 코멘트를 데이터베이스에서 서치한다.
@app.route("/get_posts", methods=['POST'])
def get_posts():
token_receive = request.cookies.get('mytoken')
try:
payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
hotel_id = request.form["hotel_id_give"]
hotel_id_int = int(hotel_id)
posts = list(db.comment.find({'hotel_id': hotel_id}).limit(20))#내림차순 20개 가져오기
hotel = list(db.hotel.find({'hotel_id': hotel_id_int}))
hotel_parse = [hotel[0]['name'],hotel[0]['hotel_image']]
print("hotel:",hotel)
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['user_id']}))
return jsonify({"result": "success", "msg": "포스팅을 가져왔습니다.","posts":posts,"hotel":hotel_parse})
except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
return redirect(url_for("login"))
except KeyError:
return redirect(url_for("login"))
except UnboundLocalError:
return redirect(url_for("login"))
- request.form으로 hotel_id를 받은 get_posts.py함수는 토큰 확인 후 hotel_id에 매치되는 코멘트를 posts에 담고 매치되는 호텔 이름과 이미지를 hotel에 담아 클라이언트로 보내준다. 이 때 likes 데이터 베이스와 연동하여 해당 코멘트가 가진 likes의 정보도 가져온다.
## get_posts.js 함수의 실행 후반부
success: function (response) {
if (response["result"] == "success") {
let posts = response["posts"];
let hotel = response["hotel"];
let check_nickname = response["check_nickname"]
let hotel_name = hotel[0];
let hotel_image = hotel[1];
let hotel_name_attach_temp = `...(코드 길이 관계상 생략)`;
$("#hotel_nameCard").append(hotel_name_attach);
for (let i = 0; i < posts.length; i++) {
let post = posts[i];
let star = posts[i]["comment_rate"];
let star_image = "⭐".repeat(star);
let class_heart = post["heart_by_me"] //삼항연산자
? "fa-heart" //트루면
: "fa-heart-o"; //폴스면
let count_heart = post["count_heart"];
let html_temp
if(check_nickname==post["nickname"]){
let html_temp = `...(코드 길이 관계상 생략)`}
else{let html_temp = `...(코드 길이 관계상 생략)`}}
$("#post-box").append(html_temp);}
- 이후 response의 딕셔너리가 포함된 리스트 형태로 데이터를 가져오고 이 데이터들을 호텔 정보를 보여주는 템플릿과 코멘트들을 보여주는 템플릿들에 붙여 코멘트들은 for문을 돌리고 각각 append로 붙여서 클라이언트에게 보여준다. 또한 get_posts().py에서 계산된 좋아요 갯수를 코멘트 하단에 붙여서 보여준다.
function toggle_like(post_id, type) {
console.log(post_id, type);
let $a_like = $(`#${post_id} a[aria-label='heart']`);
let $i_like = $a_like.find("i");
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) {
console.log("unlike");
$i_like.addClass("fa-heart-o").removeClass("fa-heart");
$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_give: "like",
},
success: function (response) {
console.log("like");
$i_like.addClass("fa-heart").removeClass("fa-heart-o");
$a_like.find("span.like-num").text(num2str(response["count"]));
},
});
}
}
- 사용자는 좋아요를 눌러 toggle_like.js 함수를 통해 서버 쪽에 눌렀다는 데이터를 보내는데 이 때 두 가지 경우가 생긴다. 사용자가 처음 좋아요를 눌러 코멘트에 긍정적인 표현을 하였을 때 addClass를 통해 긍정의 하트라는 클래스를 보여주고 한 번 더 눌러 사용자가 좋아요를 취소했을 때 취소당한 하트라고 표현하기 위해 addclass로 취소된 하트임을 보여주고 이 클릭에 대한 데이터를 서버에 보내준다.
@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({"user_id": payload["user_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,
"nickname": user_info["nickname"],
"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("login"))
except KeyError:
return redirect(url_for("login"))
except UnboundLocalError:
return redirect(url_for("login"))
- 좋아요를 눌렀다면 좋아요를 누른 사용자이므로 이에 대한 정보를 post로 서버에서 받아 likes 데이터베이스로 정보를 넘겨준다. 이 때 긍정의 좋아요 수를 카운팅하여 현재 코멘트가 얼마나 많은 좋아요를 받았는지 클라이언트에게 보여준다.
function post() {
let getLink = window.location.search;
let num = getLink.split("=")[1];
let comment = $("#textarea-post").val();
let comment_rate = $("#star").val();
$.ajax({
type: "POST",
url: "/posting",
data: {
comment_give: comment,
comment_rate_give: comment_rate,
hotel_id_give: num,
},
success: function (response) {
$("#modal-post").removeClass("is-active"); //모달을 닫는다.
window.location.reload();
},
});
}
- 사용자가 코멘트를 만들고 생성하기 버튼을 눌렀을 때 실행되며 post.js 함수로 코멘트의 내용과 평점, hotel_id를 서버로 넘겨준다.
@app.route('/posting', methods=['POST'])
def posting():
token_receive = request.cookies.get('mytoken')
try:
payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
hotel_id = request.form["hotel_id_give"]
user_info = db.users.find_one({"user_id": payload["user_id"]})
comment_receive = request.form["comment_give"]
comment_rate = request.form["comment_rate_give"]
hotel_id = request.form["hotel_id_give"]
doc = {
"nickname": user_info["nickname"],
"hotel_id": hotel_id,
"comment": comment_receive,
"comment_rate": comment_rate
}
db.comment.insert_one(doc)
return jsonify({"result": "success", 'msg': '포스팅 성공'})
except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
return redirect(url_for("login"))
except KeyError:
return redirect(url_for("login"))
except UnboundLocalError:
return redirect(url_for("login"))
- 클라이언트에서 받은 데이터들을 comment 데이터 베이스로 저장하여 코멘트가 저장된다.