본문 바로가기
파이썬/파이썬 플라스크

ep 12. flask로 instagram Clone 코딩 - 3

by L_SU 2022. 11. 1.

저번 시간엔 백엔드 작업을 해주고 시간이 좀 지났습니다..!

프로젝트 마감과 시험이 겹쳐서 이제서야 12주차 과제를 하네요



이번 시간엔 프론트엔드 작업을 해줄 것입니다.


먼저 frontend 폴더에서 assets\js\script.js 파일을 작업해줍시다.

 

다음과 같이 코드를 입력해주면 됩니다.

 

그리고 확인을 해보면 다음과 같이 에러가 발생하는 것을 확인하실 수 있습니다.

CORS에러인데 이를 해결 해주기 위해서는 백엔드에서의 작업을 필요로 합니다.

 

 

먼저 백엔드 폴더에서 가상환경을 실행시키고 위와 같은 명령어를 입력해줍니다.

 

 

그 후 api\__init__.py 에 CORS 관련 코드를 입력해줍니다.

 

그리고 다시 백엔드 서버를 켜주고 확인을 해보면 위와 같이 CORS 문제가 해결되고, 콘솔에 찍히는 것을 확인할 수 있습니다!

 

위의 주소로 가면 현재 내가 작성한 게시물들을 확인해볼 수 있는데, 저는 저번 테스트 때 작성한 게시물 하나만 존재합니다.

테스트가 원활히 진행될 수 있도록 포스트를 몇개 더 추가해봅시다!

 

먼저 위와 같이 flask shell을 이용해 shell을 활성화 시킵니다.

 

그 후 글쓴이를 지정해줘야 하기때문에 유저를 생성해보도록 하겠습니다

먼저 유저 생성을 해줘야 하기 때문에 유저모델을 import 해줍니다.

 

그 후 필드에 맞게 값을 넣어 데이터베이스를 저장합니다.

 

 

이제 글을 추가해보도록 하겠습니다.

똑같이 PostModel을 import 해줍니다.

이후 필드값을 넣어주면 됩니다. 여기서 저는 전 시간에 만들어둔 유저가 있었고, 이번 시간에 만든 유저가 있기 때문에 글쓴이를 이번 시간에 만든 유저로 해주기 위해 author_id 를 2로 주었습니다.

반복 작업으로 포스트를 더 추가해줍니다.

 

이후 ctrl+z 로 빠져나와 주시면 되겠습니다.

 

그리고 서버를 실행시켜 확인해보면 이렇게 추가가 된 모습을 확인 해보실 수 있습니다.

 

.이후 프론트로 돌아와 js 파일을 위와 같이 수정해줍니다.

 

 

수정 후 확인 해보면 이렇게 콘솔에 우리가 작성한 글들이 찍히는 것을 볼 수 있습니다.

 

하지만 코드에서 게시글을 10개 단위로 페이지네이션을 적용했기 때문에 이를 확인하기 위해 게시물들을 좀 더 작성해보도록 하겠습니다.

 

유저는 작성했기에 이번엔 Post만 import 해줍니다.

 

그리고 지금까지 작성한 게시글들을 모두 삭제하겠습니다.

아까울 수 있지만 다음 작업에서 한번에 게시글을 추가할 것이기 때문에 미련 없이 보내줍시다!

 

삭제가 됐는지 확인해줍니다.

삭제가 되었다면 위와 같이 for문을 이용해 글을 추가해줍니다.

 

잘 추가 되었네용

 

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./assets/css/style.css" type="text/css" />
    <title>Flastagram</title>
  </head>

  <body>
    <!-- navbar start -->
    <nav class="navbar">
      <div class="nav-wrapper">
        <img src="./assets/img/logo.png" class="brand-img" alt="brand image" />
        <input type="text" class="search-box" placeholder="search" />
        <div class="nav-items">
          <img src="./assets/img/home.PNG" class="icon" alt="" />
          <img src="./assets/img/messenger.PNG" class="icon" alt="" />
          <img src="./assets/img/add.PNG" class="icon" alt="" />
          <img src="./assets/img/explore.PNG" class="icon" alt="" />
          <img src="./assets/img/like.PNG" class="icon" alt="" />
          <div class="icon user-profile"></div>
        </div>
      </div>
    </nav>
    <!-- navbar end -->

    <section class="main">
      <div class="wrapper">
        <div class="left-col">
          <!-- status start -->
          <div class="status-wrapper">
            <div class="status-card">
              <div class="profile-pic">
                <img src="./assets/img/profile-ex.png" alt="pic" />
              </div>
              <p class="username">goddessana</p>
            </div>
            <div class="status-card">
              <div class="profile-pic">
                <img src="./assets/img/profile-ex.png" alt="pic" />
              </div>
              <p class="username">goddessana</p>
            </div>
            <div class="status-card">
              <div class="profile-pic">
                <img src="./assets/img/profile-ex.png" alt="pic" />
              </div>
              <p class="username">goddessana</p>
            </div>
            <div class="status-card">
              <div class="profile-pic">
                <img src="./assets/img/profile-ex.png" alt="pic" />
              </div>
              <p class="username">goddessana</p>
            </div>
            <div class="status-card">
              <div class="profile-pic">
                <img src="./assets/img/profile-ex.png" alt="pic" />
              </div>
              <p class="username">goddessana</p>
            </div>
            <div class="status-card">
              <div class="profile-pic">
                <img src="./assets/img/profile-ex.png" alt="pic" />
              </div>
              <p class="username">goddessana</p>
            </div>
            <div class="status-card">
              <div class="profile-pic">
                <img src="./assets/img/profile-ex.png" alt="pic" />
              </div>
              <p class="username">goddessana</p>
            </div>
            <div class="status-card">
              <div class="profile-pic">
                <img src="./assets/img/profile-ex.png" alt="pic" />
              </div>
              <p class="username">goddessana</p>
            </div>
            <div class="status-card">
              <div class="profile-pic">
                <img src="./assets/img/profile-ex.png" alt="pic" />
              </div>
              <p class="username">goddessana</p>
            </div>
            <div class="status-card">
              <div class="profile-pic">
                <img src="./assets/img/profile-ex.png" alt="pic" />
              </div>
              <p class="username">goddessana</p>
            </div>
            <div class="status-card">
              <div class="profile-pic">
                <img src="./assets/img/profile-ex.png" alt="pic" />
              </div>
              <p class="username">goddessana</p>
            </div>
          </div>
          <!-- status end -->

          <!-- single post -->
          <div class="post">
            <div class="info">
              <div class="user">
                <div class="profile-pic">
                  <img src="./assets/img/add.PNG" alt="" />
                </div>
                <p class="author"></p>
              </div>
              <img src="./assets/img/option.PNG" class="options" alt="" />
            </div>
            <img src="./assets/img/cover 1.png" class="post-image" alt="" />
            <div class="post-content">
              <div class="reaction-wrapper">
                <img src="./assets/img/like.PNG" class="icon" alt="" />
                <img src="./assets/img/comment.PNG" class="icon" alt="" />
                <img src="./assets/img/send.PNG" class="icon" alt="" />
                <img src="./assets/img/save.PNG" class="save icon" alt="" />
              </div>
              <p class="likes">1,999,231 likes</p>
              <p class="description"></p>
              <span class="author"></span>-
              <span class="title"></span>
              <p class="content"></p>
              <p class="post-time"></p>
            </div>
            <div class="comment-wrapper">
              <img src="./assets/img/smile.PNG" class="icon" alt="pic" />
              <input
                type="text"
                class="comment-box"
                placeholder="Add a comment!"
              />
              <button class="comment-btn">post</button>
            </div>
          </div>
          <!-- single post -->
        </div>

        <div class="right-col">
          <div class="profile-card">
            <div class="profile-pic">
              <img src="./assets/img/profile-ex.png" alt="" />
            </div>
            <div>
              <p class="username">discus</p>
              <p class="sub-text">blue diamond.</p>
            </div>
            <button class="action-btn">switch</button>
          </div>
          <p class="suggestion-text">Suggestions for You</p>
          <div class="profile-card">
            <div class="profile-pic">
              <img src="./assets/img/profile-ex.png" alt="" />
            </div>
            <div>
              <p class="username">discus</p>
              <p class="sub-text">blue diamond.</p>
            </div>
            <button class="action-btn">switch</button>
          </div>
          <div class="profile-card">
            <div class="profile-pic">
              <img src="./assets/img/profile-ex.png" alt="" />
            </div>
            <div>
              <p class="username">discus</p>
              <p class="sub-text">blue diamond.</p>
            </div>
            <button class="action-btn">switch</button>
          </div>
          <div class="profile-card">
            <div class="profile-pic">
              <img src="./assets/img/profile-ex.png" alt="" />
            </div>
            <div>
              <p class="username">discus</p>
              <p class="sub-text">blue diamond.</p>
            </div>
            <button class="action-btn">switch</button>
          </div>
          <div class="profile-card">
            <div class="profile-pic">
              <img src="./assets/img/profile-ex.png" alt="" />
            </div>
            <div>
              <p class="username">discus</p>
              <p class="sub-text">blue diamond.</p>
            </div>
            <button class="action-btn">switch</button>
          </div>
          <div class="profile-card">
            <div class="profile-pic">
              <img src="./assets/img/profile-ex.png" alt="" />
            </div>
            <div>
              <p class="username">discus</p>
              <p class="sub-text">blue diamond.</p>
            </div>
            <button class="action-btn">switch</button>
          </div>
        </div>
      </div>
    </section>
    <script src="./assets/js/script.js"></script>
  </body>
</html>

여기까지의 index.html 파일의 코드입니다.

 

이제 scripts.js 파일을 좀 더 추가해주도록 하겠습니다.

const postListBaseURL = "http://127.0.0.1:5000/posts/"; // url 주소를 가져옴(api 기본 url 정의)

async function getPostListDatafromAPI() {
  //리스트를 가져오는데, 안 될 시 에러를 찍음
  try {
    const somePromise = await fetch(postListBaseURL);
    const result = somePromise.json();
    return result;
  } catch (error) {
    console.log(error);
  }
}

function copyDiv() {
  // post Div 전체를 복사
  const postDiv = document.querySelector(".post");
  const newNode = postDiv.cloneNode(true);

  newNode.id = "copied-posts";
  postDiv.after(newNode);
}

function loadPosts() {
  // 게시물 목록 데이터를 불러와서 페이지 네이션 처리를 해 게시물을 그림
  getPostListDatafromAPI()
    .then((result) => {
      for (let i = 0; i < result.length; i++) {
        copyDiv();
        const authorNameElements = document.querySelectorAll(".author");
        for (const authorName in authorNameElements) {
          authorNameElements[authorName].innerText =
            result[9 - i]["author_name"];
        }
        const titleElement = document.querySelector(".title");
        titleElement.innerText = result[9 - i]["title"];
        const contentElement = document.querySelector(".content");
        contentElement.innerText = result[9 - i]["content"];
        if (i == 0) {
          document.getElementById("copied-posts").style.display = "none";
        }
      }
    })
    .catch((error) => {
      console.log(error);
    });
}

loadPosts(); // 최종 함수 호출

 

이후 확인 하기 이전에!

전 시간에 제가 까먹고 코드를 수정하지 않은 부분이 있어 지금 단계에서 바로 수정해주도록 하겠습니다.

백엔드 단에서 post.py에 get 함수를 다음과 같이 처리하도록 바꿔주어야 합니다.

 

이후 프론트에서 다시 index.html 파일을 확인해보면

이렇게 잘 출력 되는 것을 확인 해볼 수 있습니다.

 

기초 작업이긴 했지만 이렇게 우리는 백엔드에서 만든 api를 프론트단에서 호출해 사용해봤습니다! 박수 한번 치고 갑시다 짝짝짝..

 

다음으론 회원가입을 구현해보도록 하겠습니다.

 

회원가입 API 구현

먼저 user 모델 파일에서 아래와 같이 코드를 추가합니다.

코드를 보면 아시겠지만 email로 유저를 찾을 수 있는 그런 것이라 생각하시면 되겠습니다.

 

백엔드 폴더의 resources 폴더에 user.py 파일을 생성해줍니다.

from api.models.user import UserModel
from flask_restful import Resource, reqparse


_user_parser = reqparse.RequestParser()
_user_parser.add_argument("username", type=str, required=True, help="This field cannot be blank.")
_user_parser.add_argument("password", type=str, required=True, help="This field cannot be blank.")
_user_parser.add_argument("email", type=str, required=True, help="This field cannot be blank.")


class UserRegister(Resource):

    def post(self):
        data = _user_parser.parse_args()

        if UserModel.find_by_username(data["username"]):
            return {"message": "A user with that username already exists"}, 400

        elif UserModel.find_by_email(data["email"]):
            return {"message": "A user with that email already exists"}, 400

        user = UserModel(**data)
        user.save_to_db()

        return ({"message": "User created successfully."},)

다음과 같은 코드를 입력해주시면 됩니다. 어려운 코드가 없어 몇개만 빠르게 보고 넘어가겠습니다.

기본적으로 post 요청을 받아 데이터를 체크합니다.

이때 이미 존재하는 이름, 이메일일 경우엔 저장되지 않습니다.

중복되지 않음 데이터 베이스에 저장하는 그런 코드입니다.

 

만든 register를 api\__init__.py에 import 해줍니다.

이후 추가해주면 됩니다.

 

 

 

postman 테스트로 api를 확인 해보면 됩니다.

 

db browser

 

여기서 끝내고 싶지만 이대로 냅두면 db에 접근할 수 있는 모든 사람이 비밀번호를 알 수 있게 됩니다..

이를 해결하기 위해 비밀번호를 암호화 시키도록하겠습니다.

 

from api.ma import ma
from marshmallow.fields import String
from marshmallow import validates_schema
from marshmallow.exceptions import ValidationError
from api.models.user import UserModel
from marshmallow import fields

fields.Field.default_error_messages["required"] = "해당 필드를 입력해 주세요."
fields.Field.default_error_messages["validator_failed"] = "해당 필드에 대한 검증이 실패했습니다."
fields.Field.default_error_messages["null"] = "해당 필드는 null 이 될 수 없습니다."


class UserRegisterSchema(ma.SQLAlchemyAutoSchema):
    password_confirm = String(required=True)

    class Meta:
        load_instance = True
        model = UserModel
        load_only = [
            "username",
            "email",
            "password",
            "password_confirm",
        ]

    @validates_schema
    def validate_password(self, data, **kwargs):
        if data["password"] != data["password_confirm"]:
            raise ValidationError("비밀번호가 일치하지 않습니다.", "password_confirm")

Schemas 폴더에서 user.py 파일을 만들어 다음과 같은 코드를 입력해줍니다. 보면 password_confirm 이라는 필드가 눈에 밟힐텐데 이는 비밀번호를 한번 더 입력해 원래 입력한 비밀번호와 같은지를 확인해주는 필드입니다.

 

그 후 조금 변경된 resources\user.py 파일을 수정해줍니다.

from api.models.user import UserModel
from flask_restful import Resource, request
from api.schemas.user import UserRegisterSchema

register_schema = UserRegisterSchema()


class UserRegister(Resource):
    
    def post(self):
        data = request.get_json()
        validate_result = register_schema.validate(data)
        if validate_result:
            return validate_result, 400
        else:
            if UserModel.find_by_username(data["username"]):
                return {"bad request": "중복된 사용자 이름입니다."}, 400
            elif UserModel.find_by_email(data["email"]):
                return {"message": "중복된 이메일입니다."}, 400
            user = register_schema.load(data)
            user.save_to_db()
            return {"success": f"{user.username} 님, 가입을 환영합니다!"}, 201

 

postman으로 다시 테스트 해보면 결과를 확인할 수 있습니다. 여기까지 디테일을 살려준 부분이었고 이제 진~짜 마지막으로 비밀번호 해싱 해보도록 하겠습니다.

 

 

회원가입 부분을 다음과 같이 비밀번호가 해싱될 수 있도록 변경하여 저장하도록 바꿉니다.

해싱이 되면 102글자가 되기 때문에 글자수 제한도 바꿔줍니다.

이때 데이터베이스가 꼬였을 거기 때문에.. 데이터베이스를 새로 갈아엎어줍시다..ㅠㅠ 원래는 이러면 안 됩니다.

 

순서는 데이터베이스 삭제 -> flask db init -> flask db migrate -> flask db upgrade 입니다.

 

이후 다시 postman으로 회원가입 진행후 데이터베이스를 확인해보면 이렇게 해싱되어 있는 데이터를 확인하실 수 있습니다..!