내일배움캠프 D+16(웹개발 심화반4)

enyo9rt·2022년 5월 3일

TIL-S

목록 보기
12/79

💻 웹 프로그래밍 A-Z 심화 4주차


4-2 Bulma

Bulma
부트스트랩 같은 CSS 프레임워크의 일종


4-3 회원가입 기능

해시함수 단방향 암호화 알고리즘.
해시함수 SHA256은 항상 똑같은 길이(256비트)의 암호화된 값으로 변환함.
HMAC SHA256의 경우 비밀키를 혼합해 한 번 더 해싱한다.

로그인 정보 DB에 넣기 전 암호화

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

해시메소드 참고


4-4 로그인 기능

  1. (회원가입) id,pw,nick 전달 >> id, 해시 암호화된 pw,nick 저장
  2. (로그인) id, pw 전달 >> id, 해시 암호화된 pw 찾기
  3. (찾으면) 시크릿 키로 암호화된 JWT 토큰(id, 만료시간 exp) 발급
  4. (발급받으면) 쿠키에 저장하고 만료 시간이 다 되면 쿠키에서 찾을 수 없으므로 새로고침하면 로그인이 풀리게 됨.

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: '/'});

4-7 로그인&회원가입 페이지

클래스에 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')

4-8 회원가입 페이지 기능

아이디 중복확인

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'})

4-9 로그인 페이지 기능

아이디와 비밀번호 서버로 전달!
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이 있으면 성공!


4-10 메인 페이지 모습 만들기

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에서 불러왔을 때 추가될 카드 형태를 미리 만들어둔다.


4-11 메인 페이지 - 포스팅

저번처럼 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>&nbsp;<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}로 교체!


4-12 메인 페이지 - 좋아요

어떤 사람이 좋아요 눌렀는지
어떤 글을 좋아요 눌렀는지
버튼이 여럿이면 어떤 버튼을 누른 건지(하트,별 등)
눌렀을 때 끈 건지, 켠 건지

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>
&nbsp;
<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>

4-13 프로필 페이지

  1. 포스팅 id가 token id와 같으면 정보 수정 가능하게
  2. id가 좋아요한 글들만 나오도록
  3. 로그아웃 기능

4-14 프로필 페이지 - 수정

로그아웃
버튼을 누르면 쿠키가 지워지는 함수가 실행되도록!

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

4-15 프로필 페이지 기능 완성

@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만 같았어도 이럴 일은 없었는데ㅠㅠ 아무튼 저장해두고 답안대로도 해봐야겠다.

0개의 댓글