본문 바로가기

스파르타코딩 공부내용 정리/웹개발 플러스

4주차_Sweeter(온라인 메신저) 만들기

1. 1~3주차와 차이점

1) Bulma로 예쁜 웹사이트를 만든다. (기존에는 Bootstrap)

 

 

2. 목표 : 로그인 기능을 구현할 수 있다.

 

 

3. 로그인 기능

JWT(JSON Web Token) : JSON 객체를 사용해 정보를 안정성 있게 전달하는 웹표준

1) 로그인 매서드

- 로그인 시, 비밀번호를 같은 방법으로 암호화한 후, DB에서 해당 아이디와 비밀번호를 갖는 회원이 있는지 찾습니다.

  회원 정보가 없는 경우 실패 메시지를 보내고, 찾은 경우 아이디와 토큰 만료 시간을 저장하는 토큰을 만들어 넘겨줍니다.

- 로그인 성공 메시지를 받으면 건네받은 토큰을 쿠키로 저장하여 만료되기 전까지 갖고 있으면서, API 요청을 보낼 때마다

  회원임을 확인받습니다.

- 로그아웃 시 해당 토큰을 삭제합니다.

 

2) 알려진 문제점

JWT 최신 버전을 사용한다면 다음과 같은 애러 메시지를 볼 수 있습니다.

jwt attribute error: 'str' object has no attribute 'decode'

문제점 : Login 기능이 작동하지 않습니다.

해결책 : PYJWT version을 1.7.1 version으로 다운그레이드 합니다.

 

 

4. Bulma

1) 사이트

https://bulma.io/documentation/

 

Bulma: Free, open source, and modern CSS framework based on Flexbox

Bulma is a free, open source CSS framework based on Flexbox and built with Sass. It's 100% responsive, fully modular, and available for free.

bulma.io

 

2) 장점

순수 CSS로 커스터마이징의 폭이 Bootstrap에 비해 매우 넓다.

예쁜 디자인에 최신의 느낌이 나는 기능을 구현하기 쉽다.

 

 

5. 정규표현식

1) 정의 : 제한 조건을 걸어 확인하도록 하는 컴퓨터가 이해할 수 있는 식

 

2) 아이디, 비밀번호 정규표현식

function is_nickname(asValue) {
    var regExp = /^(?=.*[a-zA-Z])[-a-zA-Z0-9_.]{2,10}$/;
    return regExp.test(asValue);
}

function is_password(asValue) {
    var regExp = /^(?=.*\d)(?=.*[a-zA-Z])[0-9a-zA-Z!@#$%^&*]{8,20}$/;
    return regExp.test(asValue);
}

 

 

6. app.py의 get_posts()

강의에서 놓친점 : 코드스니팻을 복사하여 붙여넣었을 때 return jsonify에 post가 없습니다.

코드를 아래와 같이 수정해야 합니다.

return jsonify({"result": "success", "msg": "포스팅을 가져왔습니다.", "posts": posts})

 

 

7. CSS / JS 분리하기

이 두개 모두 분리하여 별도의 파일로 만든 후, import해와서 해결할 수 있다.

 

 

8. 과제

게시글에 "하트"가 있는 것처럼 그 옆에 "별"과 "좋아요" 넣어주기 (하트와 동작 동일)

 

 

9. 전체 코드

1) app.py

from pymongo import MongoClient
import jwt
import datetime
import hashlib
from flask import Flask, render_template, jsonify, request, redirect, url_for
from werkzeug.utils import secure_filename
from datetime import datetime, timedelta


app = Flask(__name__)
app.config["TEMPLATES_AUTO_RELOAD"] = True
app.config['UPLOAD_FOLDER'] = "./static/profile_pics"

SECRET_KEY = 'SPARTA'

client = MongoClient("나의 몽고Atlas URL")
db = client.dbsparta_plus_week4


@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="로그인 정보가 존재하지 않습니다."))


@app.route('/login')
def login():
    msg = request.args.get("msg")
    return render_template('login.html', msg=msg)


@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"))


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

        return jsonify({'result': 'success', 'token': token})
    # 찾지 못하면
    else:
        return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})


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


@app.route('/sign_up/check_dup', methods=['POST'])
def check_dup():
    username_receive = request.form['username_give']
    exists = bool(db.users.find_one({"username": username_receive}))
    # print(value_receive, type_receive, exists)
    return jsonify({'result': 'success', 'exists': exists})


@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"))


@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"]
        print(type(date_receive))
        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.posts.insert_one(doc)
        return jsonify({"result": "success", 'msg': '포스팅 성공'})
    except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
        return redirect(url_for("home"))


@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'])
        my_username = payload["id"]
        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": my_username}))

            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": my_username}))

            post["count_like"] = db.likes.count_documents({"post_id": post["_id"], "type": "like"})
            post["like_by_me"] = bool(db.likes.find_one({"post_id": post["_id"], "type": "like", "username": my_username}))

        return jsonify({"result": "success", "msg": "포스팅을 가져왔습니다.", "posts": posts})
    except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
        return redirect(url_for("home"))


@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})
        print(count)
        return jsonify({"result": "success", 'msg': 'updated', "count": count})
    except (jwt.ExpiredSignatureError, jwt.exceptions.DecodeError):
        return redirect(url_for("home"))


if __name__ == '__main__':
    app.run('0.0.0.0', port=5000, debug=True)

 

2) login.html

<!doctype html>
<html lang="en">
    <head>

        <!-- Webpage Title -->
        <title>Log In | SWEETER</title>
        <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
        <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
        <meta property="og:title" content="스위터 - 세상을 달달하게"/>
        <meta property="og:description" content="mini project for Web Plus"/>
        <meta property="og:image" content="{{ url_for('static', filename='ogimg.png') }}"/>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <!-- Bulma CSS -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
        <!-- Font Awesome CSS -->
        <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

        <link rel="preconnect" href="https://fonts.gstatic.com">
        <link href="https://fonts.googleapis.com/css2?family=Gamja+Flower&family=Stylish&display=swap" rel="stylesheet">
        <link href="{{ url_for('static', filename='mystyle.css') }}" rel="stylesheet">

        <!-- JS -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>


        <style>


            .title {
                font-weight: 800;
                font-size: 5rem;
                font-family: 'Stylish', sans-serif;
            }

            .subtitle {
                font-family: 'Gamja Flower', cursive;
                font-size: 2rem;

            }

            .help {
                color: gray;
            }
        </style>
        <script>
            // {% if msg %}
                //    alert("{{ msg }}")
                // {% endif %}

            function toggle_sign_up() {
                $("#sign-up-box").toggleClass("is-hidden")
                $("#div-sign-in-or-up").toggleClass("is-hidden")
                $("#btn-check-dup").toggleClass("is-hidden")
                $("#help-id").toggleClass("is-hidden")
                $("#help-password").toggleClass("is-hidden")
                $("#help-password2").toggleClass("is-hidden")
            }

            function is_nickname(asValue) {
                var regExp = /^(?=.*[a-zA-Z])[-a-zA-Z0-9_.]{2,10}$/;
                return regExp.test(asValue);
            }

            function is_password(asValue) {
                var regExp = /^(?=.*\d)(?=.*[a-zA-Z])[0-9a-zA-Z!@#$%^&*]{8,20}$/;
                return regExp.test(asValue);
            }

            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")
                $.ajax({
                    type: "POST",
                    url: "/sign_up/check_dup",
                    data: {
                        username_give: username
                    },
                    success: function (response) {

                        if (response["exists"]) {
                            $("#help-id").text("이미 존재하는 아이디입니다.").removeClass("is-safe").addClass("is-danger")
                            $("#input-username").focus()
                        } else {
                            $("#help-id").text("사용할 수 있는 아이디입니다.").removeClass("is-danger").addClass("is-success")
                        }
                        $("#help-id").removeClass("is-loading")

                    }
                });
            }

            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({
                    type: "POST",
                    url: "/sign_up/save",
                    data: {
                        username_give: username,
                        password_give: password
                    },
                    success: function (response) {
                        alert("회원가입을 축하드립니다!")
                        window.location.replace("/login")
                    }
                });

            }

            function sign_in() {
                let username = $("#input-username").val()
                let password = $("#input-password").val()

                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) {
                        if (response['result'] == 'success') {
                            $.cookie('mytoken', response['token'], {path: '/'});
                            window.location.replace("/")
                        } else {
                            alert(response['msg'])
                        }
                    }
                });
            }


        </script>

    </head>
    <body>
        <section class="hero is-white">
            <div class="hero-body has-text-centered" style="padding-bottom:1rem;margin:auto;">
                <h1 class="title is-sparta">SWEETER</h1>
                <h3 class="subtitle is-sparta">세상을 달달하게</h3>
            </div>
        </section>
        <section class="section">
            <div class="container">
                <div class="box" style="max-width: 480px;margin:auto">
                    <article class="media">
                        <div class="media-content">
                            <div class="content">
                                <div class="field has-addons">
                                    <div class="control has-icons-left" style="width:100%">
                                        <input id="input-username" class="input" type="text" placeholder="아이디">
                                        <span class="icon is-small is-left"><i class="fa fa-user"></i></span>
                                    </div>
                                    <div id="btn-check-dup" class="control is-hidden">
                                        <button class="button is-sparta" onclick="check_dup()">중복확인</button>
                                    </div>

                                </div>
                                <p id="help-id" class="help is-hidden">아이디는 2-10자의 영문과 숫자와 일부 특수문자(._-)만 입력 가능합니다.</p>
                                <p id="help-id-login" class="help is-danger"></p>

                                <div class="field">
                                    <div class="control has-icons-left">
                                        <input id="input-password" class="input" type="password" placeholder="비밀번호">
                                        <span class="icon is-small is-left"><i class="fa fa-lock"></i></span>
                                    </div>
                                    <p id="help-password" class="help is-hidden">영문과 숫자 조합의 8-20자의 비밀번호를 설정해주세요.
                                        특수문자(!@#$%^&*)도 사용 가능합니다.</p>
                                    <p id="help-password-login" class="help is-danger"></p>

                                </div>


                            </div>
                            <div id="div-sign-in-or-up" class="has-text-centered">
                                <nav class="level is-mobile">
                                    <button class="level-item button is-sparta" onclick="sign_in()">
                                        로그인
                                    </button>

                                </nav>
                                <hr>
                                <h4 class="mb-3">아직 회원이 아니라면</h4>
                                <nav class="level is-mobile">

                                    <button class="level-item button is-sparta is-outlined"
                                            onclick="toggle_sign_up()">
                                        회원가입하기
                                    </button>
                                </nav>
                            </div>

                            <div id="sign-up-box" class="is-hidden">
                                <div class="mb-5">
                                    <div class="field">
                                        <div class="control has-icons-left" style="width:100%">
                                            <input id="input-password2" class="input" type="password"
                                                   placeholder="비밀번호 재입력">
                                            <span class="icon is-small is-left"><i class="fa fa-lock"></i></span>
                                        </div>
                                        <p id="help-password2" class="help is-hidden">비밀번호를 다시 한 번 입력해주세요.</p>

                                    </div>
                                </div>
                                <nav class="level is-mobile">
                                    <button class="level-item button is-sparta" onclick="sign_up()">
                                        회원가입
                                    </button>
                                    <button class="level-item button is-sparta is-outlined" onclick="toggle_sign_up()">
                                        취소
                                    </button>
                                </nav>
                            </div>


                        </div>
                    </article>
                </div>

            </div>
        </section>

    </body>
</html>

 

3) index.html

<!doctype html>
<html lang="en">
    <head>

        <!-- Webpage Title -->
        <title>Home | SWEETER</title>
        <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
        <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
        <meta property="og:title" content="스위터 - 세상을 달달하게"/>
        <meta property="og:description" content="mini project for Web Plus"/>
        <meta property="og:image" content="{{ url_for('static', filename='ogimg.png') }}"/>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <!-- Bulma CSS -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
        <!-- Font Awesome CSS -->
        <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

        <link rel="preconnect" href="https://fonts.gstatic.com">
        <link href="https://fonts.googleapis.com/css2?family=Gamja+Flower&family=Stylish&display=swap" rel="stylesheet">
        <link href="{{ url_for('static', filename='mystyle.css') }}" rel="stylesheet">

        <!-- JS -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>
        <script src="{{ url_for('static', filename='myjs.js') }}"></script>


        <script>
            $(document).ready(function () {
                get_posts()
            })

        </script>

    </head>
    <body class="has-navbar-fixed-top">
        <nav class="navbar is-fixed-top is-white" role="navigation" aria-label="main navigation">
            <div class="navbar-brand">
                <a class="navbar-item" href="/">
                    <img src="{{ url_for('static', filename='logo.png') }}">
                    <strong class="is-sparta"
                            style="font-family: 'Stylish', sans-serif;font-size: larger;">SWEETER</strong>
                </a>
            </div>
        </nav>
        <section class="section">
            <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="무슨 생각을 하고 계신가요?"
                                   onclick='$("#modal-post").addClass("is-active")'>
                        </p>
                    </div>
                </div>
            </article>
            <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>

        </section>
        <section class="section">
            <div id="post-box" class="container">
                <div class="box">
                    <article class="media">
                        <div class="media-left">
                            <a class="image is-64x64" href="#">
                                <img class="is-rounded"
                                     src={{ url_for("static", filename="profile_pics/profile_placeholder.png") }} alt="Image">
                            </a>
                        </div>
                        <div class="media-content">
                            <div class="content">
                                <p>
                                    <strong>홍길동</strong> <small>@username</small> <small>10분 전</small>
                                    <br>
                                    글을 적는 칸
                                </p>
                            </div>
                            <nav class="level is-mobile">
                                <div class="level-left">
                                    <a class="level-item is-sparta" aria-label="heart"
                                       onclick="toggle_like('', '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>

            </div>
        </section>
    </body>
</html>

 

4) user.html

<!doctype html>
<html lang="en">
    <head>

        <!-- Webpage Title -->
        <title>{{ user_info.profile_name }} | SWEETER</title>
        <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
        <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
        <meta property="og:title" content="스위터 - 세상을 달달하게"/>
        <meta property="og:description" content="mini project for Web Plus"/>
        <meta property="og:image" content="{{ url_for('static', filename='ogimg.png') }}"/>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <!-- Bulma CSS -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
        <!-- Font Awesome CSS -->
        <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

        <link rel="preconnect" href="https://fonts.gstatic.com">
        <link href="https://fonts.googleapis.com/css2?family=Gamja+Flower&family=Stylish&display=swap" rel="stylesheet">
        <link href="{{ url_for('static', filename='mystyle.css') }}" rel="stylesheet">

        <!-- JS -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>
        <script src="{{ url_for('static', filename='myjs.js') }}"></script>


        <script>
            $(document).ready(function () {
                get_posts("{{ user_info.username }}")
            })

            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: "/update_profile",
                    data: form_data,
                    cache: false,
                    contentType: false,
                    processData: false,
                    success: function (response) {
                        if (response["result"] == "success") {
                            alert(response["msg"])
                            window.location.reload()

                        }
                    }
                });
            }

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

    </head>
    <body class="has-navbar-fixed-top">
        <nav class="navbar is-fixed-top is-white" role="navigation" aria-label="main navigation">
            <div class="navbar-brand">
                <a class="navbar-item" href="/">
                    <img src="{{ url_for('static', filename='logo.png') }}">
                    <strong class="is-sparta"
                            style="font-family: 'Stylish', sans-serif;font-size: larger;">SWEETER</strong>
                </a>


            </div>

        </nav>

        <section class="hero is-white">
            <div class="hero-body" style="padding-bottom:1rem;margin:auto;min-width: 400px">
                <article class="media">

                    <figure class="media-left" style="align-self: center">
                        <a class="image is-96x96" href="#">
                            <img class="is-rounded" src="{{ url_for('static', filename=user_info.profile_pic_real) }}">
                        </a>
                    </figure>
                    <div class="media-content">
                        <div class="content">
                            <p>
                                <strong>{{ user_info.profile_name }}</strong> <small>@{{ user_info.username }}</small>
                                <br>
                                {{ user_info.profile_info }}
                            </p>
                        </div>

                    </div>
                </article>
                {% if status %}
                    <nav id="btns-me" class="level is-mobile" style="margin-top:2rem">
                        <a class="button level-item has-text-centered is-sparta" aria-label="edit"
                           onclick='$("#modal-edit").addClass("is-active")'>
                            프로필 수정&nbsp;&nbsp;&nbsp;<span class="icon is-small"><i class="fa fa-pencil"
                                                                                   aria-hidden="true"></i></span>
                        </a>

                        <a class="button level-item has-text-centered is-sparta is-outlined" aria-label="logout"
                           onclick="sign_out()">
                            로그아웃&nbsp;&nbsp;&nbsp;<span class="icon is-small"><i class="fa fa-sign-out"
                                                                                 aria-hidden="true"></i></span>
                        </a>
                    </nav>

                    <div class="modal" id="modal-edit">
                        <div class="modal-background" onclick='$("#modal-edit").removeClass("is-active")'></div>
                        <div class="modal-content">
                            <div class="box">
                                <article class="media">
                                    <div class="media-content">
                                        <div class="field">
                                            <label class="label" for="input-name">이름</label>

                                            <p class="control">

                                                <input id="input-name" class="input"
                                                       placeholder="홍길동" value="{{ user_info.profile_name }}">
                                            </p>
                                        </div>
                                        <div class="field">
                                            <label class="label" for="input-pic">프로필 사진</label>

                                            <div class="control is-expanded">
                                                <div class="file has-name">
                                                    <label class="file-label" style="width:100%">
                                                        <input id="input-pic" class="file-input" type="file"
                                                               name="resume">
                                                        <span class="file-cta"><span class="file-icon"><i
                                                                class="fa fa-upload"></i></span>
                                                    <span class="file-label">파일 선택</span>
                                                </span>
                                                        <span id="file-name" class="file-name"
                                                              style="width:100%;max-width:100%">{{ user_info.profile_pic }}</span>
                                                    </label>
                                                </div>

                                            </div>
                                        </div>
                                        <div class="field">
                                            <label class="label" for="textarea-about">나는 누구?</label>

                                            <p class="control">

                                            <textarea id="textarea-about" class="textarea"
                                                      placeholder="자기소개하기">{{ user_info.profile_info }}</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="update_profile()">업데이트</a>
                                                </div>
                                                <div class="level-item">
                                                    <a class="button is-sparta is-outlined"
                                                       onclick='$("#modal-edit").removeClass("is-active")'>취소</a>
                                                </div>
                                            </div>
                                        </nav>
                                    </div>
                                </article>
                            </div>
                        </div>
                        <button class="modal-close is-large" aria-label="close"
                                onclick='$("#modal-edit").removeClass("is-active")'></button>
                    </div>
                {% endif %}
            </div>
        </section>
        {% if status %}
            <section id="section-post" class="section">
                <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="무슨 생각을 하고 계신가요?"
					                             onclick='$("#modal-post").addClass("is-active")'>
                            </p>
                        </div>
                    </div>
                </article>
                <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>

            </section>
        {% endif %}
        <section class="section">
            <div id="post-box" class="container">

            </div>
        </section>

    </body>
</html>

 

5) mystyle.css

body {
    background-color: RGBA(232, 52, 78, 0.2);
    min-height: 100vh;
}

.section {
    padding: 1rem 1.5rem;
    max-width: 750px;
    margin: auto;
}

.is-sparta {
    color: #e8344e !important;
}

.button.is-sparta {
    background-color: #e8344e;
    border-color: transparent;
    color: #fff !important;
}

.button.is-sparta.is-outlined {
    background-color: transparent;
    border-color: #e8344e;
    color: #e8344e !important;
}

.modal-content {
    width: 600px;
    max-width: 80%;
}

input::-webkit-calendar-picker-indicator {
    display: none;
}

.image img {
    object-fit:cover;
    width:100%;
    height:100%;
}

 

6) myjs.js

function post() {
    let comment = $("#textarea-post").val()
    let today = new Date().toISOString()
    $.ajax({
        type: "POST",
        url: "/posting",
        data: {
            comment_give: comment,
            date_give: today
        },
        success: function (response) {
            $("#modal-post").removeClass("is-active")
            window.location.reload()
        }
    })
}

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 class_star = post['star_by_me'] ? "fa-star": "fa-star-o"
                    let class_like = post['like_by_me'] ? "fa-thumbs-up" : "fa-thumbs-o-up"

                    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">${num2str(post["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">${num2str(post["count_star"])}</span>
                                                        </a>
                                                        <a class="level-item is-sparta" aria-label="like" onclick="toggle_like('${post['_id']}', 'like')">
                                                            <span class="icon is-small"><i class="fa ${class_like}"
                                                                                           aria-hidden="true"></i></span>&nbsp;<span class="like-num">${num2str(post["count_like"])}</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) + "일 전"
    }
    return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`
}

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
}

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", "like": "fa-thumbs-up"}
    let class_o = {"heart": "fa-heart-o", "star": "fa-star-o", "like": "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_give: "like"
            },
            success: function (response) {
                console.log("like")
                $i_like.addClass(class_s[type]).removeClass(class_o[type])
                $a_like.find("span.like-num").text(num2str(response["count"]))
            }
        })

    }
}

 

 

10. 파이참 python interpreter

패키지 명 버전
Flask 2.1.3
Jinja2 3.1.2
MarkupSafe 2.1.1
PyJWT 1.7.1
Werkzeug 2.1.2
click 8.1.3
colorama 0.4.5
h5py 2.10.0
importlib-metadata 4.12.0
itsdangerous 2.1.2
numpy 1.23.1
pip 22.1.2
pymongo 4.1.1
setuptools 63.2.0
six 1.16.0
wheel 0.37.1
zipp 3.8.1