3주차 내일배움캠프 개발일지

2022. 1. 6. 11:45개발일지/내일배움캠프 WIL

3주차 개발일지를 이제 쓰는 이유...
9 to 9 수업이 끝나고 알바를 하는 중인데
도저히 til과 wil을 쓸 시간이 없더라구요.

게다가 팀 프로젝트를 시작하고 기능 구현을 위해
고민하는 시간이 길어지다 보니
상대적으로 덜 중요한 블로그 작성을 잠시 쉴 수밖에 없었습니다.

그래서 이제서야 3주차 개발일지를 쓰게 되었답니다.

이번 일지는 인스타그램 클론 코딩과 관련한 내용을 담아보려 합니다.

 

우선 팀 프로젝트로 진행했으며 12월 27일부터 1월 4일까지 진행했습니다.

팀 인원은 총 5명이고

HTML, CSS 담당 1명

aws, ec2 베포 및 전체 기획 담당 1명

메인 페이지 담당 1명

로그인 페이지 담당 1명

마이 페이지(프로필) 담당 1명

으로 구성했습니다.

 

저는 마이 페이지 담당이고

마이 페이지 상단에는 프로필 화면을 보여주며

하단에는 나의 게시물을 모아볼 수 있도록 하는 기능을 구현하기로 했습니다.

 

팀원 중에서 가장 개발 실력이 부족하기 때문에

주어진 시간 동안 공부를 열심히 하고

팀원분들과 튜터님의 도움을 받아가며

제가 담당한 기능을 최소한으로 구현하는데 성공했습니다.

 

  • app.py 설정
# 프로필 화면 기본 정보들입니다. mongodb에서 GET 요청으로 원하는 조건의 데이터를 불러왔습니다.
@app.route('/profile', methods=['GET'])
def profile_info():
    # key값인 id 값을 불러옵니다.(pip install 내부 라이브러리 사용 시 requests 사용)
    id = request.args.get('id')
    if id:
        userinfo = db.users.find_one({"id": id}) #현재 보고 있는 user id 값을 찾습니다.
        profile_img = return_img(userinfo) # return_img 함수를 사용하여 profile_img를 정의합니다.
        posts = list(db.posts.find({'user.id': userinfo['id']})) # mongodb posts list에서 해당 id와 일치하는 posts를 가져옵니다.
        # render_template 를 사용하여 불러온 정보들을 profile.html에서 보여줄 수 있도록 합니다.
        return render_template('profile.html', user=userinfo, profile_img=profile_img, posts=posts)
    else:
        userinfo = check_token() # mongodb에서 로그인한 유저id와 일치하는 id를 찾습니다.
        profile_img = return_img(userinfo) # return_img 함수를 사용하여 로그인한 유저의 profile_img를 정의합니다.
        posts = list(db.posts.find({'user.id':userinfo['id']})) # mongodb posts list에서 해당 id와 일치하는 posts를 가져옵니다.
        # render_template 를 사용하여 불러온 정보들을 profile.html에서 보여줄 수 있도록 합니다.
        return render_template('profile.html', user=userinfo, profile_img=profile_img, posts=posts)

팀원들이 구현한 페이지에서 user가 회원가입을 하고 로그인하게 되면 각 user마다 고유한 token을 발급받게 됩니다.

else 부분의 check_token은 아래의 함수 설정으로 저장된 user의 정보만을 불러올 수 있게 합니다.

def check_token():
    # 현재 이용자의 컴퓨터에 저장된 cookie 에서 mytoken 을 가져옵니다.
    token_receive = request.cookies.get('token')
    # token을 decode하여 payload를 가져오고, payload 안에 담긴 유저 id를 통해 DB에서 유저의 정보를 가져옵니다.
    payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
    return db.users.find_one({'id': payload['id']})

한편 이미지 파일을 저장하고 불러오는 경우에는 gridfs를 사용하면 좋습니다.

from flask import Flask, render_template, request, jsonify, redirect, url_for, send_file
from pymongo import MongoClient
import certifi
import gridfs
import codecs
from bson.objectid import ObjectId

ca = certifi.where()

client = MongoClient('여기에 주소를 넣도록 하십시오.')

db = client.dbSpace

app = Flask(__name__)

# Token 발행용 SECRET_KEY 설정
SECRET_KEY = '쉽게 알 수 없도록 설정하십시오.'

# gridfs 초기화
fs = gridfs.GridFS(db)

# JWT 패키지를 사용합니다. (설치해야할 패키지 이름: PyJWT)
import jwt

# 토큰에 만료시간을 줘야하기 때문에, datetime 모듈도 사용합니다.
import datetime

팀이 사용한 것들 입니다. 그리고 이미지를 불러오는 함수를 만들어 줍니다.

gridfs로 저장된 이미지 파일은 바이너리 값으로 저장이 되어집니다.

따라서 이를 읽기 위해 base64를 사용합니다.

# 이미지 리턴 함수
def return_img(userinfo):
    profile_img_binary = fs.get(userinfo["img"])
    profile_img_base64 = codecs.encode(profile_img_binary.read(), 'base64')
    return profile_img_base64.decode('utf-8')

# flask에서 만든 함수를 그대로 jinja2에 적용하기 위해 context_processor 데코레이터를 사용합니다.
# gridfs로 저장한 이미지는 애초에 여러 파일로 나눠지고 binary값으로 저장되기 때문에 post 데이터에 img객체를 그대로 집어넣지 못합니다.
@app.context_processor
def utility_processor():
    def return_profile_img(user_id):
        user = db.users.find_one({'_id':user_id})
        profile_img_binary = fs.get(user["img"])
        profile_img_base64 = codecs.encode(profile_img_binary.read(), 'base64')
        return profile_img_base64.decode('utf-8')
    # return값을 다음과 같이 설정하여 템플릿에 return_profile_img(post.user) 와 같이 사용가능합니다.
    return dict(return_profile_img = return_profile_img)
    
# 이미지 파일 전송
import hashlib

그리고 flask에서 만든 함수를 그대로 jinja2에서 사용하기 위해 위와 같이 작성을 했습니다.

이 부분은 팀원분들과 튜터님의 도움을 받았습니다.

 

이렇게 개인의 정보를 불러오는데 성공했으니 페이지 간 연결이 제대로 되어야 하겠다고 생각했습니다.

그래서 js파일을 만들어 메인페이지로 돌아가는 함수를 우선적으로 만들었습니다.

더하여 프로필 편집과 로그아웃, 회원탈퇴의 기능까지 구현을 위해 다음과 같이 코드를 작성했습니다.

 

  • profile.js
$(document).ready(function () {

});

/*회원탈퇴 및 로그아웃 하기*/
function sign_out(del) {
    $.removeCookie('token', {path: '/'}) // $.removeCookie('쿠키이름'); => false, 정상적인 삭제 불가
                                            // $.removeCookie('쿠키이름', { path: '/' }); => true , // 정상적 삭제 가능
    if (del === "del") {
        alert('정상적으로 회원탈퇴 되었습니다!') // "POST 요청에 따라 del(회원탈퇴)이 맞다면 쿠키를 삭제하도록 한다.
    } else {
        alert('정상적으로 로그아웃 되었습니다!') // 그게 아니라면 로그아웃만 하도록 한다.
    }
    window.location.href = "/login" // 로그인 페이지로 이동합니다.
}

/*메인페이지로 돌아가는 함수.*/

function to_main() {
    window.location.href = "/"
}

/*프로필 편집 창으로 가는 함수.*/

function to_profile_edit() {
    window.location.href = "/profile/update"
}

/*회원탈퇴 진행*/
function remove() {
    check = confirm('정말 삭제 하겠습니까?') // 데이터 삭제 전에 확인 메시지 창을 띄워줍니다.
    // !x 부정연산자 방식으로 모든 falsy 값(빈 문자열, 0, null, false, undefined 등)을 true로 return 합니다.
    // 즉 확인을 누르지 않는다면 아무일도 회원탈퇴는 일어나지 않습니다.
        if(!check){
            return
        }
    $.ajax({
        type: "POST", //POST 요청은 서버의 상태나 데이터를 변경시킬 때 사용합니다. 데이터를 삭제하기 위해 POST 요청을 하는 것이 바람직합니다.
        url: "/api/user_delete", // GET요청과 달리 ?를 사용하지 않습니다.
        data: {},
        success: function (response) {
            sign_out("del")
        }
    });
}
  • profile_update.html(script)
function profileUpdate() {
 
    // 이메일 편집
    let email = $("#email").val();
    // let var1 = $("#...").val() 에서 #을 사용하면 id 속성을, .을 사용하면 class 속성을 선택할 수 있습니다.
    // .val()은 선택한 입력 요소에 지정한 값을 반환합니다. 이메일 값을 넣지 않으면 아래와 같은 alert를 띄웁니다.
    if (email == "") {
        alert('e-mail 주소를 입력해 주세요.')
        return;
    }
    // 프로필 사진 편집
    let profile_img; // let 명령문을 통해 profile_img 지역 변수를 선언합니다.
    if ($("#profile-img")[0].files[0]) {
        profile_img = $("#profile-img")[0].files[0];
        // console.log(response)
        // 이미지파일만 올리도록 유효성 검사 실시
        if (!is_img($("#profile-img").val())) {
            alert('이미지 파일만 업로드 가능합니다.');
            return;
        }
    }
    // 설명 편집
    let description = $("#description").val();
    // FormData를 사용하는 이유는 이미지를 ajax로 업로드 하기 위해서 입니다.
    // var formData = new FormData();
    // formData.append('img',document.getElementById('file input').files[0])
    // 이렇게 선언하면, 'img'라는 key 값으로 input에 담긴 file이 value 값으로 들어갑니다.
    let formData = new FormData()
    formData.append('email', email);
    // 프로필 이미지를 바꿀때만 서버에 데이터를 보냅니다.
    if (profile_img != undefined) {
        formData.append('profile_img', profile_img); 
    }                                 
    formData.append('description', description);

    $.ajax({
        type: 'POST', // http 타입
        url: '/profile/update', // 호출 url
        cache: false, // false를 주게 되면 브라우저 캐시 저장을 막아서 현재 값을 호출할 수 있습니다.
                        // 그 이유는 url 호출을 할 때마다 새롭게 호출을 할 필요가 있기 때문입니다.
        contentType: false,
        // POST 방식으로 전송할 때 contentType의 종류 중 파일 전송을 위해서는 multipart/form-data 를 사용합니다.
        // 바이너리 데이터를 효과적으로 전송하는 것이 가능합니다.
        // ajax를 multipart 방식으로 데이터를 보낼 때 false 설정이 일반적입니다.
        processData: false,
        // ajax 방식으로 보낼 때, processData는 서버에 전달되는 data가 쿼리 스트링 형태로 전달되어 집니다.
        // 파일전송에는 이를 피해야함으로 false로 설정해줍니다.
        data: formData, // url 호출 시 보낼 (파라미터) 데이터
        success: function (response) {
            alert(response['msg'])
            window.location.replace('/profile')            
        }
    });
}

// 이미지 처리 유효성 검사
function is_img(file) {
    let regExp = /(.*?)\.(jpg|jpeg|png|JPG|JPEG|PNG)$/;
    return regExp.test(file);
}

프로필 정보를 불러오는 것은 스스로 생각하여 기능을 구현할 수 있었지만

프로필 업데이트 부분은 조금 어려워서 팀원 분의 도움을 받게 되었습니다. 

프론트 쪽 구현이 어느정도 진행되니

이제 프로필 편집, 회원탈퇴, 로그아웃 등의 기능을 구현하기 위해 app.py를 수정했습니다.

 

  • app.py
## 프로필 업데이트 기능,
@app.route('/profile/update', methods=['GET', 'POST'])
def profile_update():
    # 쿠키에서 토큰 정보를 받고 이를 통해 현재 user를 조회합니다.
    token_receive = request.cookies.get('token')
    payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
    user = db.users.find_one({'id': payload['id']})        
    # AJAX 통신으로 데이터를 전달 받습니다.
    if request.method == 'POST':        
        email = request.form['email']
        description = request.form['description']                      
        # profile 이미지를 바꾸는 경우 form에서 profile_img 키를 탐색 후 있으면 DB에 업데이트 합니다.
        if "profile_img" in request.files :
            print('exist')                   
            fs.delete(user['img'])
            profile_img = request.files['profile_img']            
            fs_image_id = fs.put(profile_img)
            db.users.update_one({'id':user['id']},{'$set':{'email':email, 'description':description, 'img':fs_image_id}})
        else:
            db.users.update_one({'id':user['id']},{'$set':{'email':email, 'description':description}})
        return jsonify({'msg': '프로필을 수정하였습니다.'})
        
    else:
        profile_img = return_img(user)
               
        return render_template('profile_update.html', user = user, profile_img = profile_img)

이후 팀장님의 손길을 거쳐서 웹페이지가 완성되었습니다.

Around Space라는 주제를 정했습니다.

홈페이지 컨셉까지 깊이 생각을 하진 않았으나

다음에는 이러한 부분까지 고려하여 더 좋은

웹페이지를 만들 수 있다면 좋겠네요!

 

https://github.com/seunghwan13/insta_clone_flask

 

GitHub - seunghwan13/insta_clone_flask: flask 인스타 클론 프로젝트 입니다.

flask 인스타 클론 프로젝트 입니다. Contribute to seunghwan13/insta_clone_flask development by creating an account on GitHub.

github.com


팀장님의 git 저장소에서 fork를 통해 가져온 전체 프로젝트 내용입니다.

 

그럼 다음주에봐요!