Bulma
부트스트랩 같은 CSS 프레임워크의 일종
해시함수 단방향 암호화 알고리즘.
해시함수 SHA256은 항상 똑같은 길이(256비트)의 암호화된 값으로 변환함.
HMAC SHA256의 경우 비밀키를 혼합해 한 번 더 해싱한다.
로그인 정보 DB에 넣기 전 암호화
pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()
JWT JSON Web Token
놀이공원 자유이용권 떠올리기!
최초에만 로그인을 하고 이후에 하지 않더라도 다른 행동을 할 때마다 서버와 클라이언트는 확인을 하고 있다.
쿠키
페이지에 관계 없이 브라우저에 딕셔너리(키: 밸류) 형태로 임시 저장되는 정보
토큰 넘겨주고
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})
print(count)
return jsonify({"result": "success", 'msg': 'updated', "count": count})
except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
return redirect(url_for("home"))
저장하고 $.cookie('mytoken', response['token'], {path: '/'});
가져오고 token_receive = request.cookies.get('mytoken')
지우고 $.removeCookie('mytoken', {path: '/'});
클래스에 is-hidden 추가하면 가려짐
function toggle_sign_up() {
if ($('#sign-up-box').hasClass('is-hidden')) {
$('#sign-up-box').removeClass('is-hidden')
} else {
$('#sign-up-box').addClass('is-hidden')
}
}
제이쿼리 toggleClass() 함수를 쓰면 훨씬 줄일 수 있다.
$('#sign-up-box').toggleClass('is-hidden')
아이디 중복확인
function check_dup() {
let username = $("#input-username").val() // 사용자가 입력한 아이디값
console.log(username)
if (username == "") { // 입력값이 없으면
$("#help-id").text("아이디를 입력해주세요.").removeClass("is-safe").addClass("is-danger")
$("#input-username").focus()
return;
}
if (!is_nickname(username)) { // 정규식에 맞지 않으면
$("#help-id").text("아이디의 형식을 확인해주세요. 영문과 숫자, 일부 특수문자(._-) 사용 가능. 2-10자 길이").removeClass("is-safe").addClass("is-danger")
$("#input-username").focus()
return;
}
$("#help-id").addClass("is-loading") // 확인 중일 때 is-loading 클래스 추가
$.ajax({
type: "POST",
url: "/sign_up/check_dup",
data: {
username_give: username
},
success: function (response) {
if (response["exists"]) { // DB에 존재하면 is-danger 클래스 추가
$("#help-id").text("이미 존재하는 아이디입니다.").removeClass("is-safe").addClass("is-danger")
$("#input-username").focus()
} else { // DB에 존재하지 않으면 is-success 클래스 추가 + 이미 존재하는 아이디를 체크했을 수도 있으니 is-danger 클래스 삭제
$("#help-id").text("사용할 수 있는 아이디입니다.").removeClass("is-danger").addClass("is-success")
}
$("#help-id").removeClass("is-loading") // 확인 끝나고 is-loading 클래스 삭제
}
});
}
bool() True/False값 반환
@app.route('/sign_up/check_dup', methods=['POST'])
def check_dup():
username_receive = request.form['username_give'] # 사용자가 입력한 id값 받아서
exists = bool(db.users.find_one({"username": username_receive})) # DB에 있는지 확인하고 True/False 값 넣기
return jsonify({'result': 'success', 'exists': exists})
회원가입
function sign_up() { // 회원가입 하기 버튼을 누르면 실행
let username = $("#input-username").val()
let password = $("#input-password").val()
let password2 = $("#input-password2").val()
console.log(username, password, password2)
// 중복확인이 알맞게 되었는지 체크
if ($("#help-id").hasClass("is-danger")) {
alert("아이디를 다시 확인해주세요.")
return;
} else if (!$("#help-id").hasClass("is-success")) {
alert("아이디 중복확인을 해주세요.")
return;
}
if (password == "") { // 비밀번호 입력값이 없으면
$("#help-password").text("비밀번호를 입력해주세요.").removeClass("is-safe").addClass("is-danger")
$("#input-password").focus()
return;
} else if (!is_password(password)) { // 비밀번호가 정규식에 맞지 않으면
$("#help-password").text("비밀번호의 형식을 확인해주세요. 영문과 숫자 필수 포함, 특수문자(!@#$%^&*) 사용가능 8-20자").removeClass("is-safe").addClass("is-danger")
$("#input-password").focus()
return
} else { // 비밀번호가 정규식에 맞으면
$("#help-password").text("사용할 수 있는 비밀번호입니다.").removeClass("is-danger").addClass("is-success")
}
if (password2 == "") { // 재확인 비밀번호 입력값이 없으면
$("#help-password2").text("비밀번호를 입력해주세요.").removeClass("is-safe").addClass("is-danger")
$("#input-password2").focus()
return;
} else if (password2 != password) { // 재확인 비밀번호와 비밀번호 입력값이 다르면
$("#help-password2").text("비밀번호가 일치하지 않습니다.").removeClass("is-safe").addClass("is-danger")
$("#input-password2").focus()
return;
} else { // 재확인 비밀번호와 비밀번호 입력값이 같으면
$("#help-password2").text("비밀번호가 일치합니다.").removeClass("is-danger").addClass("is-success")
}
$.ajax({ // DB에 아이디와 비밀번호 전달
type: "POST",
url: "/sign_up/save",
data: {
username_give: username,
password_give: password
},
success: function (response) {
alert("회원가입을 축하드립니다!")
window.location.replace("/login")
}
});
}
@app.route('/sign_up/save', methods=['POST'])
def sign_up():
username_receive = request.form['username_give']
password_receive = request.form['password_give']
password_hash = hashlib.sha256(password_receive.encode('utf-8')).hexdigest()
doc = {
"username": username_receive, # 아이디
"password": password_hash, # 암호화된 비밀번호
"profile_name": username_receive, # 프로필 이름 기본값은 아이디
"profile_pic": "", # 프로필 사진 파일 이름
"profile_pic_real": "profile_pics/profile_placeholder.png", # 프로필 사진 기본 이미지
"profile_info": "" # 프로필 한 마디
}
db.users.insert_one(doc)
return jsonify({'result': 'success'})
아이디와 비밀번호 서버로 전달!
DB에 아이디와 비밀번호가 일치하는 데이터가 있으면 클라이언트에 토큰 발급!
클라이언트는 쿠키에 그 토큰을 저장! 끝!
function sign_in() {
let username = $("#input-username").val()
let password = $("#input-password").val()
// 아이디와 비밀번호 입력값이 있는지 없는지 확인 + 미리 경고내용이 들어갈 id 만들어주기
if (username == "") {
$("#help-id-login").text("아이디를 입력해주세요.")
$("#input-username").focus()
return;
} else {
$("#help-id-login").text("")
}
if (password == "") {
$("#help-password-login").text("비밀번호를 입력해주세요.")
$("#input-password").focus()
return;
} else {
$("#help-password-login").text("")
}
$.ajax({ // 입력값이 있으니 전달!
type: "POST",
url: "/sign_in",
data: {
username_give: username,
password_give: password
},
success: function (response) { // 서버가 DB 확인을 끝내고 발급해 준 토큰을 받아옴
if (response['result'] == 'success') {
$.cookie('mytoken', response['token'], {path: '/'}); // 발급받은 토큰을 쿠키에 저장
window.location.replace("/")
} else {
alert(response['msg'])
}
}
});
}
@app.route('/sign_in', methods=['POST'])
def sign_in():
# 로그인
username_receive = request.form['username_give']
password_receive = request.form['password_give']
pw_hash = hashlib.sha256(password_receive.encode('utf-8')).hexdigest()
result = db.users.find_one({'username': username_receive, 'password': pw_hash})
if result is not None: # 찾으면
payload = {
'id': username_receive,
'exp': datetime.utcnow() + timedelta(seconds=60 * 60 * 24) # 로그인 24시간 유지
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256') # JWT 토큰 발급
return jsonify({'result': 'success', 'token': token})
# 찾지 못하면
else:
return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})
쿠키가 있는지 확인하기
검사 >> Application >> Cookies 에 mytoken이 있으면 성공!
CSS 시트 연결
<link href="{{ url_for('static', filename='mystyle.css') }}" rel="stylesheet">
Bluma CSS 시트보다 아래에 지정해야함. (그 CSS를 수정하기 때문)
내비게이션 바
모든 페이지의 가장 상단에 있는, 목록과 로고가 나오는 바
포스팅 칸 옆 프로필 기능
<figure class="media-left" style="align-self: center">
{# 프로필 이미지를 클릭하면 해당 유저의 이름이 적혀있는 url, 즉 프로필 페이지로 이동 #}
<a class="image is-32x32" href="/user/{{ user_info.username }}">
{# uesr_info의 profile_pic_real을 불러옴 #}
<img class="is-rounded" src="{{ url_for('static', filename=user_info.profile_pic_real) }}">
</a>
</figure>
@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="로그인 정보가 존재하지 않습니다."))
모달 modal
특정 입력이나 동작을 했을 때 페이지 위로 나타나는 작은 입력 창
bulma에서는 is-active 클래스가 있는지에 따라 동작함.
취소 버튼을 누르든 x를 누르든 배경을 누르든 꺼지게 해둠.
<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="무슨 생각을 하고 계신가요?"></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()">포스팅하기</a>
</div>
<div class="level-item">
<a class="button is-sparta is-outlined"
onclick='$("#modal-post").removeClass("is-active")'>취소</a>
</div>
</div>
</nav>
</div>
</article>
</div>
</div>
<button class="modal-close is-large" aria-label="close"
onclick='$("#modal-post").removeClass("is-active")'></button>
</div>
포스팅 카드
나중에 DB에서 불러왔을 때 추가될 카드 형태를 미리 만들어둔다.
저번처럼 id에 i값을 넣거나 하지 않고 _id를 이용한다!
최신 포스트를 가져와서 _id 값으로 id를 구분해준다
@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'])
posts = list(db.posts.find({}).sort("date", -1).limit(20)) # 최신 포스트 20개 가져오기
for post in posts:
post["_id"] = str(post["_id"]) # _id 값 문자열로 변환
return jsonify({"result": "success", "msg": "포스팅을 가져왔습니다."})
except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
return redirect(url_for("home"))
function get_posts() {
$("#post-box").empty()
$.ajax({
type: "GET",
url: "/get_posts",
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 html_temp = `<div class="box" id="${post["_id"]}"> // id 값은 _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_post}</small>
<br>
${post['comment']}
</p>
</div>
<nav class="level is-mobile">
<div class="level-left">
<a class="level-item is-sparta" aria-label="heart"token interpolation">${post['_id']}', 'heart')">
<span class="icon is-small"><i class="fa fa-heart"
aria-hidden="true"></i></span> <span class="like-num">2.7k</span>
</a>
</div>
</nav>
</div>
</article>
</div>`
$("#post-box").append(html_temp)
}
}
}
})
}
포스팅 시간 정리하기
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) + "일 전"
} // 작성한 지 7일이 넘어가면 날짜로 나타내기
return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`
}
posts 배열의 [date] 값만 넣었던 time_post를 time_before에 넣어서 정리해준다.
let time_post = new Date(post["date"])
let time_before = time2str(time_post)
let time_post = new Date(post["date"])
let time_before = time2str(time_post)
temp_html 안의 ${time_post}는 ${time_before}로 교체!
어떤 사람이 좋아요 눌렀는지
어떤 글을 좋아요 눌렀는지
버튼이 여럿이면 어떤 버튼을 누른 건지(하트,별 등)
눌렀을 때 끈 건지, 켠 건지
html_temp에서 heart_by_me 값 주기
let class_heart = ""
if (post["heart_by_me"]) {
class_heart = "fa-heart"
} else {
class_heart = "fa-heart-o"
}
기존 i 태그의 클래스 중 fa-heart 는 ${class_heart}로 교체!
상위 6줄의 코드는 조건부 삼항 연산자를 써서 이렇게 한 줄로 대체할 수 있다
let class_heart = post['heart_by_me'] ? "fa-heart": "fa-heart-o"
조건 ? true일 때 : false일 때
좋아요 카운트 숫자 변수 선언 해주기
let count_heart = post['count_heart']
${post["count_heart"]}
좋아요 형식 바꿔주기
숫자가 1000이 넘어갈 경우 K로 대체
// get_posts()
<span class="icon is-small"><i class="fa ${class_heart}" aria-hidden="true"></i></span>
<span class="like-num">${num2str(post["count_heart"])}</span>
// toggle_like()
$a_like.find("span.like-num").text(num2str(response["count"]))
js 겹치니까 따로 빼서 import
<script src="{{ url_for('static', filename='myjs.js') }}"></script>
로그아웃
버튼을 누르면 쿠키가 지워지는 함수가 실행되도록!
function sign_out() {
$.removeCookie('mytoken', {path: '/'});
alert('로그아웃!')
window.location.href = "/login"
}
@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"]) # 내 프로필이면 True, 다른 사람 프로필 페이지면 False
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"))
매개변수 username과 토큰 id값이 일치하는지 status로 확인할 수 있다.
본인이 아닐 경우 없어져야 할 부분 숨기기
1) 프로필 페이지에서 포스트 입력하는 부분
2) 프로필 수정 및 로그아웃
secure_file()
참고
SAVE 💾
숙제 얼레벌레 하고 답안 보니까 딕셔너리 형태로 묶어서 넣어줬다... 나는 if문을 엄청 써서 넣었다. icon class만 같았어도 이럴 일은 없었는데ㅠㅠ 아무튼 저장해두고 답안대로도 해봐야겠다.