wisePocket

[Flask] Flask framework 미니프로젝트(project bucket) 09 (v1.1 업데이트 기능 추가 및 배포) 본문

Python&Flask Tutorials, AWS EB/Flask_project_bucket

[Flask] Flask framework 미니프로젝트(project bucket) 09 (v1.1 업데이트 기능 추가 및 배포)

ohnyong 2023. 8. 8. 20:09

현재까지 미니 프로젝트에서 DB의 insert, read 기능만 구현했지만 이번엔 update기능을 추가했다.

신규 버전의 업데이트 내용은 다음과 같다.

  • NEW RELEASE
    • 2023.08.08 - v1.1 완료 상태 업데이트 기능 추가 및 AWS EB 배포
      • 저장된 버킷리스트의 '완료' or '미완료' 상태를 구분 할 수 있는 DB내 done컬럼 추가
      • 앞으로 DB내 신규 생성되는 버킷 리스트글은 글 번호, done 상태 컬럼이 추가되어 입력 됨
      • 상태 기능 추가에 따른 뷰 페이지 버킷리스트 목록에 상태 변경 버튼 추가
      • 버튼을 통해 '완료' 상태로 전환 또는 잘못 누른 경우를 위한 '복구' 기능 추가
      • 기본적인 유효성 체크(상태 변경 버튼에 대한 확인 or 취소) 알림창 팝업 추가
      • AWS EB를 통한 배포 추가

 

부분 코드는 아래 노트에 기록했지만 전체 코드는 다음과 같다.

app.py

더보기
# 라이브러리 임포트
# Flask Framework
# view페이지 렌더링을 위한 render_template 메서드
# 요청 데이터에 접근 할 수 있는 flask.request 모듈
# dictionary를 json형식의 응답 데이터를 내보낼 수 있는 jsonify 메서드
from flask import Flask, render_template, request, jsonify
app = Flask(__name__)

 

# [POST-3] MongoDB사용을 위한 pymongo와 certifi 임포트
from pymongo import MongoClient
import certifi
# [POST-4] DB 커넥션 구성
ca = certifi.where()
client = MongoClient('mongodb+srv://<user>:<pass>@<url>?retryWrites=true&w=majority',tlsCAFile=ca)
db = client.dbsparta

 

# "localhost:5001/" URL요청에 메인 뷰 페이지 반환 응답
@app.route('/')
def home():
return render_template('index.html')

 

# [POST-0] CREATE 부분부터 코드를 작성하는 것(==POST)이 확인이 가능(READ부터하면 데이터가없어서 테스트 어려움)
# fetch('URL')부분, 반환값은 res로 전달.
# "localhost:5001/bucket" URL POST방식 요청에 응답
@app.route("/bucket", methods=["POST"])
def bucket_post():
# [POST-1] 프론트로부터 무엇을 받아야 하는가? -> 프론트 input으로부터 (html)bucket->(js)bucket_give를 받을 것이다.
bucket_receive = request.form['bucket_give']

 

# ----start----[추가된 부분]
bucket_list = list(db.bucket.find({}, {'_id': False}))
count = len(bucket_list) + 1
# ----end----[추가된 부분]

 

print(bucket_receive)
# [POST-2] 클라이언트로부터 받은 데이터를 DB에 넣자 MongoDB연결을 위한 임포트부터 시작(더블체크)
# [POST-5] DB연결이 완료되었으니 Dictionary key:value형태 데이터들을 doc=리스트 객체에 담는다.
# INSERT_ONE
# 저장 - 예시
# doc = {'name':'bobby','age':21}
doc = {
# ----start----[추가된 부분]
'num':count, #버킷 등록 시, db에서 특정 버킷을 찾기 위해 'num' 이라는 고유 값 부여
'done' : 0, #'done' key값을 추가 해 각 버킷의 완료 상태 구분(0 = 미완료, 1 = 완료)
# ----end----[추가된 부분]
'bucket' : bucket_receive
}
# [POST-6] doc에 담았으니 DB에 insert 한다.
db.bucket.insert_one(doc)
# [POST-7] insert가 완료되었으니 완료 메시지를 반환한다.
return jsonify({'msg': 'POST 연결 완료!'+'DB 저장 완료!'})
 
# ----start----[추가된 부분]
# 등록한 버킷을 ‘완료’ 상태로 바꾸는 기능을 만들어 주세요
# - 이미지 참고 (▶️ 버튼을 눌러주세요)
# 1. 등록한 버킷을 ‘완료’ 상태로 바꾸는 기능을 만들어 주세요. 등록한 버킷의 우측에 ‘완료’ 버튼을 생성하고, 버튼을 클릭 시 버킷 문구 뒤에 ‘완료!!’ 가 추가되면 완성입니다!
# - [완료!] 버튼 클릭 시 → [완료!!] 텍스트 생기도록 하기
# 추가된 업데이트문
@app.route("/update", methods=["POST"])
def bucket_update():
num_receive = request.form['num_give']
print(num_receive)
#해당글 찾아서
find_content_filter = {'num': int(num_receive)}
#업데이트 할 부분 key부분 set
update = {'$set': {'done': 1}}

 

# [POST-6] doc에 담았으니 DB에 update 한다.
db.bucket.update_one(find_content_filter, update)
# [POST-7] update 완료되었으니 완료 메시지를 반환한다.
return jsonify({'msg': 'POST 연결 완료!'+'DB UPDATE 완료!'})

 

# 수정을 또 복구하는 업데이트문
@app.route("/restore", methods=["POST"])
def restore_post():
num_receive = request.form['num_give']
find_content_filter = {'num': int(num_receive)}
update = {'$set': {'done': 0}} # done 값을 0으로 업데이트

 

db.bucket.update_one(find_content_filter, update)
return jsonify({'msg': '복구 완료!'})
# ----end----[추가된 부분]

 

# [GET-0] CREATE 부분이 테스트 완료되어 DB에 자료가 추가되는 상황, READ로 View 페이지에 DB 데이터를 가져와서 보여주자.
# fetch('URL')부분, 반환값은 res로 전달.
# "localhost:5001/bucket" URL GET방식 요청에 응답
@app.route("/bucket", methods=["GET"])
def bucket_get():
# [GET-1] 필요한 데이터는? -> DB에서 API 데이터를 가져와야 한다.
all_bucket = list(db.bucket.find({},{'_id':False}))
# [GET-2] 가져온 데이터는? -> json으로 변환하여 반환 -> 프론트(js)로 이동
return jsonify({'result': all_bucket})

 

# app이라는 메인 함수
# if __name__ == "__main__" 의 의미는 메인 함수의 선언, 시작을 의미
# 이 파이썬 스크립트가 직접 실행될 때에는 main() 함수를 실행하라
if __name__ == '__main__':
app.run('0.0.0.0', port=5001, debug=True)

index.html

더보기
<!-- HTML Doctype선언문 -->
<!DOCTYPE html>
<html lang="en">

 

<head>
<!-- 인코딩 캐릭터셋 UTF-8지정 -->
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- 부트스트랩 css 참조 -->
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous" />
<!-- jquery 참조 -->
<!-- 부트스트랩 javascript 참조 -->
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<!-- Google Font 참조 -->
<!-- JS 모듈화 참조 -->
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
<!-- CSS 모듈화 참조 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<title>인생 버킷리스트</title>

 

</head>

 

<body>
<!-- Header -->
<div class="mypic">
<h1>나의 버킷리스트</h1>
</div>

 

<!-- Content -->
<div class="mybox">
<div class="mybucket">
<input id="bucket" class="form-control" type="text" placeholder="이루고 싶은 것을 입력하세요" />
<button onclick="save_bucket()" type="button" class="btn btn-outline-danger">기록하기!</button>
</div>
</div>
<div class="mybox" id="bucket-list">
<li>
<h2>✅ 호주에서 스카이다이빙 하기</h2>
</li>
</div>
 
<!-- Footer -->
<!-- NONE -->
</body>

 

</html>

style.css

더보기
* {
font-family: "Gowun Dodum", sans-serif;
}

 

.mypic {
width: 100%;
height: 200px;

 

background-image: linear-gradient(0deg,
rgba(0, 0, 0, 0.5),
rgba(0, 0, 0, 0.5)),
background-position: center;
background-size: cover;

 

color: white;

 

display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

 

.mypic>h1 {
font-size: 30px;
}

 

.mybox {
width: 95%;
max-width: 700px;
padding: 20px;
box-shadow: 0px 0px 10px 0px lightblue;
margin: 20px auto;
}

 

.mybucket {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}

 

.mybucket>input {
width: 70%;
}

 

.mybox>li {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;

 

margin-bottom: 10px;
min-height: 48px;
}

 

.mybox>li>h2 {
max-width: 75%;
font-size: 20px;
font-weight: 500;
margin-right: auto;
margin-bottom: 0px;
}

 

.mybox>li>h2.done {
text-decoration: line-through;
}



#fix_status {
display:none;
}

 

#restore_status:hover #origin_status{
display:none;
}
#restore_status:hover #fix_status{
display:block;
}

 

button:hover {
transition: 1.0s;
}

1.1 업데이트 노트

 

1. DB 설계 변경 및 insert 함수 변경 사항

insert로 등록된, read로 메인 페이지에서 보여지는 버킷 리스트 목록을 "버킷 리스트를 완료" 했다면 완료한 것으로 변경해서 보여주고싶었다. 

중요한 부분은 해당 목록에서 "선택" 된 레코드에 대해서만 완료 or 미완료를 구분해야 한다.

이를 하기 위한 전제는 각 글마다 고유한 번호가 필요했다. 따라서 insert로 입력 할 때 마다 글 번호인 num을 콘텐츠와 함께 저장되도록 했으며, 자동으로 1씩 늘어나게 설정되었다. 마치 Oracle의 sequence, MySQL의 auto_increment와 비슷한 로직으로 매번 새로운 글 번호를 저장할 수 있도록 len()함수를 이용하여 변수를 선언 할당 했다.

더하여 버킷리스트의 상태를 done이라는 필드값을 추가해서 0일때, 1일때를 구분하여 버킷리스트 완료는 1, 버킷리스트 미완료는 0으로 표기했다. 기본으로 0을 할당하여 미완료 상태를 디폴트로 insert되게 된다.

@app.route("/bucket", methods=["POST"])
def bucket_post():
    ...
    
    #전체 버킷리스트의 목록을 불러온다
    #len() 함수를 통해서 전체 목록 갯수를 구할 수 있으며 +1로 다음 글번호를 지정한다.
    
    bucket_list = list(db.bucket.find({}, {'_id': False}))
    count = len(bucket_list) + 1
    
    
    ...
    
    # insert될 doc의 내용에 num, done을 추가한다.
    
    doc = {
    # ----start----[추가된 부분]
        'num':count,  #버킷 등록 시, db에서 특정 버킷을 찾기 위해 'num' 이라는 고유 값 부여
        'done' : 0,   #'done' key값을 추가 해 각 버킷의 완료 상태 구분(0 = 미완료, 1 = 완료)
    # ----end----[추가된 부분]
        'bucket' : bucket_receive
    }
    # [POST-6] doc에 담았으니 DB에 insert 한다.
    db.bucket.insert_one(doc)
    
        return jsonify({'msg': 'POST 연결 완료!'+'DB 저장 완료!'})

script.js

더보기
// [Read]
$(document).ready(function () {
show_bucket();
$('.status-container').mouseover( function() {
$(this).find('.origin-status').fadeOut();
$(this).find('.fix-status').fadeIn();
});
$('.status-container').mouseout( function() {
$(this).find('.origin-status').fadeIn();
$(this).find('.fix-status').fadeOut();
});
});
function show_bucket() {
fetch('/bucket').then(res => res.json()).then(data => {
// json 형식으로 변환, 반환된 데이터가 res 인자로 들어옴
// res.json()에 의해 Promise 객체로 변환되어 data에 저장
// data 내용 테스트
console.log("data===>"+data)

 

// data의 내용이 OpenAPI로부터 데이터 받는것과 동일
// 리스트 형태의 data를 rows 변수에 담고
let rows = data['result']
console.log("rows===>"+rows)

 

// 반복문 전에 하드코딩 부분 비워주기
$('#bucket-list').empty();

 

// forEach 반복문을 통해
rows.forEach((a)=>{
// 리스트에 있는 key의 value들을 각 변수에 담기
let bucket = a['bucket']
let num = a['num']
let done = a['done']
console.log("bucket===>"+bucket)
let contents = ``;
if(done == 0){
contents = `<li>
<h2>👍 ${bucket}</h2>
<button onclick="update_bucket(${num})" type="button" class="btn btn-outline-primary">완료하기?</button>
<input id="content_num" type="hidden" placeholder="완료용 게시글의 번호" value="${num}"/>
</li>`
}else if(done == 1){
contents = `
<li>
<h2>✅ <span><del>${bucket}</del></span><div style="float:right">(완료!!)</div></h2>
<div class="status-container">
<button id="restore_status" onclick="restore_bucket(${num})" type="button" class="btn btn-outline-success">
<span id="origin_status">축하해요!</span>
<span id="fix_status">혹시 잘못 누르셨나요?!</span>
</button>
</div>
<input id="content_num" type="hidden" placeholder="완료용 게시글의 번호" value="${num}"/>
</li>`
}
// index.html에 위 변수들이 들어가도록 백틱 내 자리표시자${variable} 작성한 내용을 temp_html에 작성
let temp_html = contents
 
// 위 temp_html을 index.html의 #cards-box div에 붙여주기.
$('#bucket-list').append(temp_html)
})
})
}

 

// [Create]
function save_bucket() {
// index.html로부터 값 가져오기
let bucket = $('#bucket').val()

 

// formData 객체를 생성하고
let formData = new FormData();
// append()통해 {key,value}를 객체에 담는다
formData.append("bucket_give", bucket);

 

// POST 요청에 위 formData를 body에 담아 요청한다.
fetch('/bucket', { method: "POST", body: formData, }).then((response) => response.json()).then((data) => {
alert(data["msg"]);
// 브라우저 새로고침 추가
window.location.reload();
});
}

 

// [Update]
function update_bucket(num){
if (!confirm("정말 버킷리스트를 이루셨나요?/t 확인[예] / 취소[아니오]")) {
alert("취소[아니오]를 누르셨습니다.");
window.location.reload();
} else {
alert("확인[예]을 누르셨습니다.");
 
// alert(num);
// console.log(num);
let update_num = num;
let formData = new FormData();
formData.append("num_give", num);

 

fetch('/update', { method: "POST", body: formData, }).then((response) => response.json()).then((data) => {
alert(data["msg"]);
// 브라우저 새로고침 추가
window.location.reload();
});
// // formData 객체를 생성하고
// let formData = new FormData();
// // append()통해 {key,value}를 객체에 담는다
// formData.append("num_give", num);
}
}

 

function restore_bucket(num) {
if (!confirm("버킷리스트를 다시 복구할까요?? 확인[예] / 취소[아니오]")) {
alert("취소[아니오]를 누르셨습니다.");
window.location.reload();
} else {
alert("확인[예]을 누르셨습니다.");
 
let formData = new FormData();
formData.append("num_give", num);

 

fetch('/restore', { method: "POST", body: formData }).then((response) => response.json()).then((data) => {
alert(data["msg"]);
// 브라우저 새로고침 추가
window.location.reload();
});
}
}

2. bucket_update()의 업데이트 기능 함수 추가

우선 HTML 뷰페이지 단에서 나타나는 버킷리스트 목록은 js에서 append로 구성되어 있다. 따라서 버킷 리스트 자체의 수정은 js에서 우선적으로 수정한다. 일단 테스트용으로 버튼을 생성하여 onclick 이벤트로 update_bucket 함수를 부른다. 여기서 DB에서 저장된 글 번호를 대상으로 해야 한다. 글 번호를 특정하지 않으면 id를 아무리 지정해도 첫번째 글의 데이터밖에 못가져오게 된다. 따라서 상단의 bucket이라는 글 내용을 배열에 담았던것과 마찬가지로 db에 있는 num이라는 key, 또한 done이라는 key를 통해서 밸류들을 각자 배열로 저장한다.

function show_bucket() {
    fetch('/bucket').then(res => res.json()).then(data => {
		
        ...
        
        // forEach 반복문을 통해
        rows.forEach((a)=>{
            // 리스트에 있는 key의 value들을 각 변수에 담기
            let bucket = a['bucket']
            let num = a['num']
            let done = a['done']
            console.log("bucket===>"+bucket)
            let contents = ``;
            if(done == 0){
                contents =   `<li>
                                    <h2>👍 ${bucket}</h2>
                                    <button onclick="update_bucket(${num})" type="button" class="btn btn-outline-primary">완료하기?</button>
                                    <input id="content_num" type="hidden" placeholder="완료용 게시글의 번호" value="${num}"/>
                                </li>`
            }else if(done == 1){
                contents =   `
                                <li>
                                    <h2>✅ <span><del>${bucket}</del></span><div style="float:right">(완료!!)</div></h2>
                                    <div class="status-container">
                                        <button id="restore_status" onclick="restore_bucket(${num})" type="button" class="btn btn-outline-success">
                                            <span id="origin_status">축하해요!</span>
                                            <span id="fix_status">혹시 잘못 누르셨나요?!</span>
                                        </button>
                                    </div>
                                    <input id="content_num" type="hidden" placeholder="완료용 게시글의 번호" value="${num}"/>
                                </li>`
            }
            // index.html에 위 변수들이 들어가도록 백틱 내 자리표시자${variable} 작성한 내용을 temp_html에 작성
            let temp_html = contents
                                
            // 위 temp_html을 index.html의 #cards-box div에 붙여주기.
            $('#bucket-list').append(temp_html)
        })
    })
}

이후 나는 done이라는 버킷 리스트 달성 여부 필드에 따라 조건문을 통해서 두가지 형태로 append되도록 구성했다. 

디폴트 상태(insert만 된 상태)의 버킷 리스트 내용은 '완료하기' 버튼이 나타나며 다음과 같은 코드로 최소한의 유효성체크 알림창과 /update라는 url로 POST 요청을 보내도록 구성했다.

// [Update]
function update_bucket(num){
    if (!confirm("정말 버킷리스트를 이루셨나요?/t 확인[예] / 취소[아니오]")) {
        alert("취소[아니오]를 누르셨습니다.");
        window.location.reload();
    } else {
        alert("확인[예]을 누르셨습니다.");
    
        // alert(num);
        // console.log(num);
        let update_num = num;
        let formData = new FormData();
        formData.append("num_give", num);

        fetch('/update', { method: "POST", body: formData, }).then((response) => response.json()).then((data) => {
            alert(data["msg"]);
            // 브라우저 새로고침 추가
            window.location.reload();
        });
            // // formData 객체를 생성하고
            // let formData = new FormData();
            // // append()통해 {key,value}를 객체에 담는다
            // formData.append("num_give", num);
    }
}

완료된 상태인 경우 del 스타일과 완료메시지, 버튼 또한 축하해요라는 버튼으로 구분되도록 구성했다. 

 


3. restore_post()의 복구 기능 함수 추가

더하여 축하해요라는 버튼이 나타나는 완료된 버킷 리스트도 사용자가 잘못 눌렀을 가능성을 생각해서 마우스오버(hover)를 통해서 버튼이 변경되며 복구 할 수 있는 기능을 추가했다. /restore라는 url을 통해 POST로 요청하도록 한다. 이 부분에도 기본적인 확인 유효성체크는 삽입하였다.

script.js는 다음과 같으며

function restore_bucket(num) {
    if (!confirm("버킷리스트를 다시 복구할까요?? 확인[예] / 취소[아니오]")) {
        alert("취소[아니오]를 누르셨습니다.");
        window.location.reload();
    } else {
        alert("확인[예]을 누르셨습니다.");
        
    let formData = new FormData();
    formData.append("num_give", num);

    fetch('/restore', { method: "POST", body: formData }).then((response) => response.json()).then((data) => {
            alert(data["msg"]);
            // 브라우저 새로고침 추가
            window.location.reload();
        });
    }
}

app.py의 함수 부분은 다음과 같다.

# 수정을 또 복구하는 업데이트문
@app.route("/restore", methods=["POST"])
def restore_post():
    num_receive = request.form['num_give']
    find_content_filter = {'num': int(num_receive)}
    update = {'$set': {'done': 0}}  # done 값을 0으로 업데이트

    db.bucket.update_one(find_content_filter, update)
    return jsonify({'msg': '복구 완료!'})

 

해당 부분은 버튼의 onclick이벤트를 통해서 완료를 실행하면 0->1로 변경되도록, 또한 복구 버튼을 누르면 1->0으로 다시 복구 되는 기능을 구현했다. 기본적으로 함수마다 window.location.reload()를 통해서 변경 사항이 새로 로드 되도록 구현했다. 이부분은 추후 ajax를 활용해서 구현하면 웹 페이지 전체를 로드 할 필요가 없기 때문에 효율적으로 고칠 수 있을 것 같다.

 

4. AWS Elastic Beanstalk 배포

AWS EB를 통해서 Amazon 서버를 활용한 배포를 진행했다. 주소는 다음과 같다. 배포과정은 이전에 기록을 잘 기록해두어서 손쉽게 진행 할 수 있었다. 다른점은 이전에 Key Pair를 생성해두었기 때문에 한번 더 할 필요 없던점, 그리고 이전 프로젝트가 존재하는 경우 deploy로 업데이트할지, create로 새 프로젝트를 만들지 설정 하도록 되어있다. 이전 미니 프로젝트 팬명록도 우선 지울 필요는 없기 때문에 create로 새로운 어플리케이션을 생성하는 것으로 진행했다.

http://bucketlist.eba-8gmu7v8e.ap-northeast-2.elasticbeanstalk.com/

 

인생 버킷리스트

 

bucketlist.eba-8gmu7v8e.ap-northeast-2.elasticbeanstalk.com


해당 프로젝트는 아래 깃을 통해 업데이트되고 있습니다.

https://github.com/yzpocket/Flask_project_bucket

 

GitHub - yzpocket/Flask_project_bucket

Contribute to yzpocket/Flask_project_bucket development by creating an account on GitHub.

github.com