스파르타코딩클럽 내일배움단 웹개발 종합반 4주차 개발일지

Bluewiz_YSH·2022년 5월 26일
0

1. 4주차 배울 내용 & 2개 프로젝트 준비 (폴더 세팅 & Flask 시작하기)

우리가 1주차부터 저번 3주차까지 한것은 HTML 파일을 다루고, API에 대해서 배우고, MongoDB를 통해 데이터 베이스 만들고 즉, 서버에 항상 상주하는 파일들을 만드는 방법을 배우고 또 실제로 만들었다. 이제 4주차에는 서버를 직접 구동해서 이런 파일들을 올리고 직접 테스트까지 해보는 로컬 개발 환경을 구현하고 체험해볼 차례라고 강사님은 말씀하셨다. 그림으로 보면 아래와 같다.

그래서 4주차에는 2개의 프로젝트를 만들건데 그러기 위해선 먼저 연습을 하고 2개의 프로젝트를 만든 뒤 5주차에는 최종 프로젝트를 또 따로 만든다고 하셨다.

그래서 아래 설명과 같이 폴더를 4개 준비하라고 하셨다.

prac: "flask 연습 코드를 작성합니다." (오늘)
alonememo : "나홀로메모장" 관련 코드를 작성합니다. (오늘)
bookreview : "모두의책리뷰" 관련 코드를 작성합니다. (오늘)
moviestar : "마이페이보릿무비스타" 관련 코드를 작성합니다. (5주차)

일단 연습을 해야하니 prac 폴더에 들어가 app.py 파이썬 파일을 생성후 앞으로 서버 프레임워크는 flask를 이용할거니 저번처럼 똑같이 패키지 설치 방법대로 flask 프레임워크 설치를 하라고 하셨다. (flask 패키지는 아래 사진과 같다. 살짝 라이브러리와 프레임워크의 차이점도 설명하셨는데 프레임워크는 남이 짜준 틀이나 규칙안에서 자유롭게 코딩하는것이고, 라이브러리는 프레임워크와 동일하게 가져가되 중간에 남이 만든 것들을 중간에 쓰기도 한다는 것에서 차이점이 난다고 하셨다.)

그리고 아래와 같은 코드로 flask 서버를 실행시킨다.

from flask import Flask
app = Flask(__name__)

@app.route('/')
def home():
   return 'This is Home!'

if __name__ == '__main__':
   app.run('0.0.0.0',port=로컬 포트 숫자,debug=True)

그러면 아래와 같이 미리 넣었던 return 문구가 크롬에 새 탭으로 뜨면서 서버가 실행되고 있음을 알수 있다. 첫 서버를 만든 것이다.

또한, 아래 코드로 수정하면 하나의 서버에 두개의 URL을 담을 수 있어 컨텐츠 내용을 나눌수 있다.

from flask import Flask
app = Flask(__name__)

@app.route('/')
def home():
   return 'This is Home!'

@app.route('/mypage')
def mypage():  
   return 'This is My Page!'

if __name__ == '__main__':  
   app.run('0.0.0.0',port=로컬포트숫자,debug=True)

그리고 추가로 아래와 같은 이름의 폴더 2개를 프로젝트 안에 만들어둔다.

static 폴더 (이미지, css 파일들 저장)
templates 폴더 (html 파일)
app.py 파일

여기서 강사님이 templates 폴더에 index.html 파일을 만들고 아래와 같은 코드를 파일안에 넣으라고 하셨다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <title>Document</title>
</head>
<body>
    <h1>서버를 만들었다!</h1>
</body>
</html>

그러면 html 파일은 완성되었고, app.py에서 이 html 파일을 열수 있도록 또 코드를 짜야하기 때문에 아래와 같은 코드를 app.py에 수정, 입력하라고 하셨다.

from flask import Flask, render_template
app = Flask(__name__)

## URL 별로 함수명이 같거나,
## route('/') 등의 주소가 같으면 안됩니다.

@app.route('/')
def home():
   return render_template('index.html')

if __name__ == '__main__':
   app.run('0.0.0.0', port=로컬 포트 숫자, debug=True)

아까 서버를 처음 연 코드와 달라진것이 render_template, 즉 templates 폴더 안에 index.html을 넣어놓고 최상단에 render_template 임포트한 뒤 home() 함수 안 return 구문에 render_template('index_html')을 입력해두면 서버가 자동적으로 index.html을 클라이언트에 보내준다. (아래 사진과 같이 결과물이 나온다.)

그리고 이제 강사님은 본격적으로 API를 스스로 만들어서 서버에 올리는 연습을 해본다고 하셨다.
우리가 지난 시간에 API를 배우고 다룬것과 같이 클라이언트와 서버간 통신을 할때 정해진 규약이 있다고 하는 점을 다시 한번 언급하시면서 가장 많이 다루는 API 방식인 GET과 POST를 중점적으로 다뤄본다고 하셨다. (GET = 서버의 데이터 조회 요청, URL 뒤에 물음표를 붙여 key=value로 전달함. POST = 서버의 데이터 생성(Create)/변경(Update)/삭제(Delete) 요청, 바로 보이지 않는 HTML body에 key:value 형태로 전달)

그러면서 강사님은 먼저 GET 요청 실습을 먼저 보여주셨다. 아래와 같은 코드문으로 app.py를 수정하라고 하셨다.

from flask import Flask, render_template, request, jsonify
app = Flask(__name__)

@app.route('/')
def home():
   return render_template('index.html')

@app.route('/test', methods=['GET'])
def test_get():
   title_receive = request.args.get('title_give')
   print(title_receive)
   return jsonify({'result':'success', 'msg': '이 요청은 GET!'})

if __name__ == '__main__':
   app.run('0.0.0.0',port=로컬포트숫자,debug=True)

강사님은 이렇게 코드문을 작성해둠으로써 서버에 GET 요청 API '창구'를 만들어둔것이라고 하셨다. 그래서 이 창구가 제대로 작동하는지 확인을 해야하기에 아래와 같은 코드문을 서버의 콘솔창에 입력해서 결과를 한번 보라고 하셨다.

$.ajax({
    type: "GET",
    url: "/test?title_give=봄날은간다",
    data: {},
    success: function(response){
       console.log(response)
    }
  })

그러면 결과는 콘솔창 그리고 파이참 Run 창에 각각 나온다.

설명하자면, ajax 기능을 이용해 서버에 title_give 값이 봄날은간다인 url을 get 요청으로 주었고, 결과값은 콘솔 출력이었기에 미리 정해진대로 return 구문인 result : 'success'와 msg : '이 요청은 GET!' 값이 콘솔에 뜬것이며, 또 중간에 print(title_give)가 있었으므로 app.py가 실행되고 결과가 나오는 구역인 파이참 Run 창에 봄날은간다가 뜬것이다.

그럼 GET 요청은 완성이 되었고 다음은 POST 요청이었다. 강사님 지시대로 아래와 같은 코드문으로 app.py를 수정했다.

from flask import Flask, render_template, request, jsonify
app = Flask(__name__)

@app.route('/')
def home():
   return render_template('index.html')

@app.route('/test', methods=['GET'])
def test_get():
   title_receive = request.args.get('title_give')
   print(title_receive)
   return jsonify({'result':'success', 'msg': '이 요청은 GET!'})

@app.route('/test', methods=['POST'])
def test_post():
   title_receive = request.form['title_give']
   print(title_receive)
   return jsonify({'result':'success', 'msg': '이 요청은 POST!'})

if __name__ == '__main__':
   app.run('0.0.0.0',port=로컬포트숫자,debug=True)

(request.args.get을 썼던 GET과 다르게 POST는 request.form[ ]을 쓴다)

그리고 ajax POST 요청은 다음과 같다.

$.ajax({
    type: "POST",
    url: "/test",
    data: { title_give:'봄날은간다' },
    success: function(response){
       console.log(response)
    }
  })

(ajax get 요청과는 다르게 data 값으로 title_give 값이 들어가있다.)

그러면 아래와 같이 결과창이 뜬다.

GET 요청과 비슷하지만 msg 값에서 확실히 이 결과는 POST 요청으로 들어온것을 알수 있고 Run 창에 찍힌 것처럼 봄날은간다도 POST 방식으로 들어온 출력값임을 알수 있다.


2. [모두의책리뷰] - 프로젝트 세팅 & 뼈대 준비하기 & POST,GET 연습 (리뷰 저장, 리뷰 보여주기)

모두의책리뷰 예시 페이지 : http://spartacodingclub.shop/bookreview

강사님 말씀대로 이제 처음 설명했던 두개의 프로젝트 중 첫번째, 모두의 책 리뷰를 만들기 위해

File-New Project-최상단 Location에 sparta → projects → bookreview 경로로 설정해서

venv 잘 들어왔고, create main.py 체크 해제된거 확인하고 다 됐으면 create로 모두의 책 리뷰 프로젝트 생성을 했다.

그리고, flask 서버 기능은 물론 모두의 책 리뷰는 데이터도 저장해야하기에 pymongo까지 우리가 앞서 배웠던 라이브러리 설치 기능으로 설치도 했다.

또한, html이 저장될 templates 폴더 그리고 이미지 관련 파일들이 저장될 static 폴더까지 만들어주면 끝이었다.

그러면 아래 사진과 같이 모두의 책 리뷰 첫 시작 사전 준비가 완료되었다.

그리고 뼈대를 만들어야 하는데, 먼저 app.py는 아래와 같은 코드로 입력할것을 강사님이 권하셨다.

from flask import Flask, render_template, jsonify, request
app = Flask(__name__)

from pymongo import MongoClient
client = MongoClient('localhost', 로컬포트숫자)
db = client.dbsparta

## HTML을 주는 부분
@app.route('/')
def home():
    return render_template('index.html')

## API 역할을 하는 부분
@app.route('/review', methods=['POST'])
def write_review():
    sample_receive = request.form['sample_give']
    print(sample_receive)
    return jsonify({'msg': '이 요청은 POST!'})


@app.route('/review', methods=['GET'])
def read_reviews():
    sample_receive = request.args.get('sample_give')
    print(sample_receive)
    return jsonify({'msg': '이 요청은 GET!'})


if __name__ == '__main__':
    app.run('0.0.0.0', port=로컬포트숫자, debug=True)

pymongo를 임포트해 클라이언트 client와 데이터베이스 db 값을 완성했고 app.route('/') 부분에 index.html을 출력하도록 template 기능 활용했고, API는 request 기능을 이용해 POST, GET 둘 다 만들어둔것을 알수 있다.

(다 3,4주차에 배웠던 pymongo 그리고 flask의 총집합이었다.)

그리고 index.html도 꾸며야하기에 아래 코드를 index.html에 넣으면 됐다.

<!DOCTYPE html>
<html lang="ko">

    <head>
        <!-- Webpage Title -->
        <title>모두의 책리뷰 | 스파르타코딩클럽</title>

        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
              integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
              crossorigin="anonymous">

        <!-- JS -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
                integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
                crossorigin="anonymous"></script>

        <!-- 구글폰트 -->
        <link href="https://fonts.googleapis.com/css?family=Do+Hyeon&display=swap" rel="stylesheet">

        <script type="text/javascript">

            $(document).ready(function () {
                showReview();
            });

            function makeReview() {
                $.ajax({
                    type: "POST",
                    url: "/review",
                    data: {sample_give:'샘플데이터'},
                    success: function (response) {
                        alert(response["msg"]);
                        window.location.reload();
                    }
                })
            }

            function showReview() {
                $.ajax({
                    type: "GET",
                    url: "/review?sample_give=샘플데이터",
                    data: {},
                    success: function (response) {
                        alert(response["msg"]);
                    }
                })
            }
        </script>

        <style type="text/css">
            * {
                font-family: "Do Hyeon", sans-serif;
            }

            h1,
            h5 {
                display: inline;
            }

            .info {
                margin-top: 20px;
                margin-bottom: 20px;
            }

            .review {
                text-align: center;
            }

            .reviews {
                margin-top: 100px;
            }
        </style>
    </head>

    <body>
        <div class="container">
            <img src="https://previews.123rf.com/images/maxxyustas/maxxyustas1511/maxxyustas151100002/47858355-education-concept-books-and-textbooks-on-the-bookshelf-3d.jpg"
                 class="img-fluid" alt="Responsive image">
            <div class="info">
                <h1>읽은 책에 대해 말씀해주세요.</h1>
                <p>다른 사람을 위해 리뷰를 남겨주세요! 다 같이 좋은 책을 읽는다면 다 함께 행복해질 수 있지 않을까요?</p>
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text">제목</span>
                    </div>
                    <input type="text" class="form-control" id="title">
                </div>
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text">저자</span>
                    </div>
                    <input type="text" class="form-control" id="author">
                </div>
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text">리뷰</span>
                    </div>
                    <textarea class="form-control" id="bookReview"
                              cols="30"
                              rows="5" placeholder="140자까지 입력할 수 있습니다."></textarea>
                </div>
                <div class="review">
                    <button onclick="makeReview()" type="button" class="btn btn-primary">리뷰 작성하기</button>
                </div>
            </div>
            <div class="reviews">
                <table class="table">
                    <thead>
                    <tr>
                        <th scope="col">제목</th>
                        <th scope="col">저자</th>
                        <th scope="col">리뷰</th>
                    </tr>
                    </thead>
                    <tbody id="reviews-box">
                    <tr>
                        <td>왕초보 8주 코딩</td>
                        <td>김르탄</td>
                        <td>역시 왕초보 코딩교육의 명가답군요. 따라하다보니 눈 깜짝할 사이에 8주가 지났습니다.</td>
                    </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </body>

</html>

부트스트랩과 구글폰트 임포트한 부분부터 시작해 html이 준비되었을때 showReview 함수가 작동하도록 $(document).ready 기능도 있고, 당연히 showReview는 ajax GET 요청, makeReview 함수 또한 POST 설정까지 해두었고 makeReview는 "리뷰 작성하기" 버튼에 onclick 함수로 배정되어있었으며 나머지 본문 body 태그 부분과 css 디자인 부분까지 꼼꼼히 미리 구조가 짜여져 있었다.

(1,2주차때 배웠던 html 다루는법의 총집합이었다.)

그러면 이렇게 해둔 모두의책리뷰 기초 메커니즘이 잘 작동하는지 확인을 해야했기에 app.py를 실행(Run)한뒤 localhost쪽으로 들어갔다. 그랬더니 아래 그림과 같이 잘 작동하는 모습을 볼수 있었다.

뼈대로서 웹과 서버가 잘 작동하는건 일단 확인했고, 이제 일단 백그라운드인 서버 app.py를 기반으로 본문 내용에 있는 제목, 저자, 리뷰 내용을 데이터베이스 db에 전송하여 저장하고 또 그것을 잘 저장했는지 확인시켜주는 메세지 창을 띄우는 구조를 만들어야 했다.(POST API 부분) 그래서 아래와 같은 순서대로 코드를 또 수정했다.

API 는 약속이라고 했습니다. API를 먼저 만들어보죠!
리뷰를 작성하기 위해 필요한 정보는 다음 세 가지 입니다.

  • 제목(title)
  • 저자(author)
  • 리뷰(review)

따라서 API 기능은 다음 세 단계로 구성되어야 합니다.
1. 클라이언트가 준 title, author, review 가져오기.
2. DB에 정보 삽입하기
3. 성공 여부 & 성공 메시지 반환하기

정리하면, 만들 API 정보는 아래와 같습니다.

A. 요청 정보

  • 요청 URL= /review , 요청 방식 = POST
  • 요청 데이터 : 제목(title), 저자(author), 리뷰(review)

B. 서버가 제공할 기능 : 클라이언트에게 보낸 요청 데이터를 데이터베이스에 생성(Create)하고, 저장이 성공했다고 응답 데이터를 보냄

C. 응답 데이터 : (JSON 형식) 'msg'= '리뷰가 성공적으로 작성되었습니다.'

그래서 다음과 같은 코드문으로 app.py의 POST 요청을 수정했다. (예전에 연습했던 데이터베이스 연습 dbprac.py 파일 참고)

@app.route('/review', methods=['POST'])
def write_review():
    # title_receive로 클라이언트가 준 title 가져오기
    title_receive = request.form['title_give']
    # author_receive로 클라이언트가 준 author 가져오기
    author_receive = request.form['author_give']
    # review_receive로 클라이언트가 준 review 가져오기
    review_receive = request.form['review_give']

    # DB에 삽입할 review 만들기
    doc = {
        'title': title_receive,
        'author': author_receive,
        'review': review_receive
    }
    # reviews에 review 저장하기
    db.bookreview.insert_one(doc)
    # 성공 여부 & 성공 메시지 반환
    return jsonify({'msg': '리뷰가 성공적으로 작성되었습니다.'})

그러면 이제 클라이언트 부분인 index.html의 POST 요청 부분, makeReview 함수를 수정할 차례였다. 다음과 같은 순서로 수정 방향을 강사님이 알려주셨다.

클라이언트 코드는 다음 세 단계로 구성되어야 합니다.
1. input에서 title, author, review 가져오기
2. 입력값이 하나라도 없을 때 alert 띄우기.
3. Ajax로 서버에 저장 요청하고, 화면 다시 로딩하기

그래서 아래와 같은 코드문으로 makeReview 함수를 재정의했다.

function makeReview() {
    // 화면에 입력어 있는 제목, 저자, 리뷰 내용을 가져옵니다.
    let title = $("#title").val();
    let author = $("#author").val();
    let review = $("#bookReview").val();

    // POST /review 에 저장(Create)을 요청합니다.
    $.ajax({
        type: "POST",
        url: "/review",
        data: { title_give: title, author_give: author, review_give: review },
        success: function (response) {
            alert(response["msg"]);
            window.location.reload();
        }
    })
}

그렇게 서버, 클라이언트 둘 다 수정하면 아래 그림과 같이 리뷰를 저장하는 POST 요청을 했을때 잘 작동하는 것을 확인할수 있다.

(또한, 저장된 데이터베이스를 볼수 있는 ROBO 3T에도 정상적으로 bookReview 콜렉션과 함께 데이터가 저장됨을 확인하였다.)

그리고 이제 GET 요청, 모두의책리뷰 웹페이지에서 리뷰 조회 기능을 구현할 차례였다.

앞서 했던 POST 요청과 절차는 똑같다. 다만 코드 부분만 다를 뿐이었다.

먼저, 서버와 클라이언트가 잘 연결되어있는지 구조적으로도 실제 모습으로도 확인을 해야한다.
그래서 아래와 같은 내용으로 모두의 책 리뷰 GET 요청이 잘 처리되는지 점검한다.

  1. 요청 정보 : 요청 URL= /review , 요청 방식 = GET
  2. 서버가 제공할 기능 : 클라이언트에게 정해진 메시지를 보냄
  3. 응답 데이터 : (JSON 형식) {'msg': '이 요청은 GET!'}

일단 app.py 서버 부분의 GET 요청은 아래 코드처럼 잘 만들어져있다.

@app.route('/review', methods=['GET'])
def read_reviews():
    sample_receive = request.args.get('sample_give')
    print(sample_receive)
    return jsonify({'msg': '이 요청은 GET!'})

/review 주소로 GET 요청을 받을때, sample_give 값을 그대로 문자열로 sample_receive 변수에 전달, print로 콘솔에 한번 확인차 출력되고 마지막 json 형식으로 msg : 이 요청은 GET! 값이 출력된다.

그리고 index.html 클라이언트 부분의 GET 요청은 아래 코드처럼 서버 부분과 잘 매칭되어있었다.

<script>

 $(document).ready(function () {
                showReview();
            });

       function showReview() {
		// 서버의 데이터를 받아오기
		$.ajax({
        type: "GET",
        url: "/review?sample_give=샘플데이터",
        data: {},
        success: function (response) {
            alert(response["msg"]);
        }
    })
}
</script>            

$(document).ready 코드를 보다시피 문서가 로딩완료, 준비되면 showReview 함수가 실행되고, showReview 함수는 ajax GET 요청을 실행해 url에 나와있듯이 sample_give 변수에 샘플데이터가 대입되고, 이 작업이 성공(success)하면 그 결과로 response값을 받아 function 함수가 실행되며 마지막엔 function 안에 있는 alert 기능이 response의 msg를 받아 실행된다.

그러면 아래 그림과 같이 서버를 통해 문서를 열면 GET 요청이 자동적으로 실행된다.

그러면 이제 실제 리뷰 조회 기능이 담기도록 서버와 클라이언트 코드들을 뜯어고쳐야 했다.

먼저 서버 부분부터 보면 데이터베이스-bookreview에 담긴 모든 데이터들을 찾아서 끌어와야하기에 아래와 같은 방식과 코드로 app.py를 수정했다.

API 기능은 다음 단계로 구성되어야 합니다.
1. DB에서 리뷰 정보 모두 가져오기
2. 성공 여부 & 리뷰 목록 반환하기

정리하면, 만들 API 정보는 아래와 같습니다.

A. 요청 정보

  • 요청 URL= /review , 요청 방식 = GET
  • 요청 데이터 : 없음

B. 서버가 제공할 기능 : 데이터베이스에 리뷰 정보를 조회(Read)하고, 성공 메시지와 리뷰 정보를 응답 데이터를 보냄

C. 응답 데이터 : (JSON 형식) 'all_reviews'= 리뷰리스트

@app.route('/review', methods=['GET'])
def read_reviews():
    # 1. DB에서 리뷰 정보 모두 가져오기
    reviews = list(db.bookreview.find({}, {'_id': False}))
    # 2. 성공 여부 & 리뷰 목록 반환하기
    return jsonify({'all_reviews': reviews})

db.bookreview.find 기능을 이용해 데이터 전부를 가져오되 _id값은 안나오게(False)하면서 list로 전부 묶어 리스트 형태로, 그리고 이 모든 데이터들이 담긴 리스트를 reviews에 담고 또 그것을 all_reivews에 담아서 json 형식으로 출력한다.

그리고 클라이언트 부분을 서버 부분과 맞게 고치면 아래와 같다.

function showReview() {
                $.ajax({
                    type: "GET",
                    url: "/review",
                    data: {},
                    success: function (response) {
                        let reviews = response['all_reviews']
                        for (let i = 0; i < reviews.length; i++) {
                            let title = reviews[i]['title']
                            let author = reviews[i]['author']
                            let review = reviews[i]['review']

                            let temp_html = `<tr>
                                                <td>${title}</td>
                                                <td>${author}</td>
                                                <td>${review}</td>
                                            </tr>`
                            $('#reviews-box').append(temp_html)
                        }
                    }
                })
            }

url에서 따로 클라이언트에게서 받아야할 데이터가 없으므로 기존 url의 ? 뒷부분은 지워버리고 서버에서 출력해 내려준 'all_reviews'를 다시 reviews로 담고 이를 for 반복문으로 전체 돌리되, 일단 먼저 각각의 데이터마다 가진 title, author, review 값을 뽑아낸뒤 이런 값들을 포함한 temp_html에 조회된 리뷰가 갖춰야할 html문 선언해두고 마지막에 html 내용 추가 기능인 .append 기능을 이용하여 리뷰 컨텐츠 내용이 달려야할 클래스(class),reviews-box에 가져온 리뷰 데이터 값들이 붙도록 해준다.

그러면 아래 그림과 같이 데이터베이스에 저장된 리뷰가 목록으로 문서에 조회된다.


3. [나홀로메모장] - 프로젝트 세팅, API 설계하기, 조각 기능 구현하기, 뼈대 준비하기, POST GET 연습 (메모하기, 보여주기)

나홀로메모장 예시 페이지: http://spartacodingclub.shop/

강사님 말씀대로 이제 처음 설명했던 두개의 프로젝트 중 마지막 두번째, 나홀로메모장을만들기 위해

File-New Project-최상단 Location에 sparta → projects → alonememo로 설정해서

venv 잘 들어왔고, create main.py 체크 해제된거 확인하고 다 됐으면 create로 모두의 책 리뷰 프로젝트 생성을 했다.

그리고, flask 서버 기능은 물론 나홀로메모장은 url을 포함한 코멘트까지 데이터로 저장해야하기에 pymongo도 설치하고 앞서 받은 url로부터 크롤링까지 해야하기에 requests, bs4(beautifulsoup)까지 우리가 앞서 배웠던 라이브러리 설치 기능으로 설치도 했다.

또한, html이 저장될 templates 폴더 그리고 이미지 관련 파일들이 저장될 static 폴더까지 만들어주면 끝이었다.

그러면 나홀로메모장 첫 시작이자 사전 준비가 완료되었다.

이제 나홀로메모장을 구현하기 위해서 어떤 기능들이 필요하고 그 기능들이 어떤 순서로 실행되는지

담은 API를 어떻게 작동하도록 만들지 설계할 차례였다.

나홀로메모장의 기능은 일단 두 가지였다.

첫번째는, 포스팅박스에 입력된 URL과 간단 코멘트를 받아서 해당 URL에 있는 이미지, 영화제목, 링크, 요약, 코멘트를 크롤링해와서 카드 형식으로 데이터를 만들고 그 데이터를 DB에 저장하는 포스팅 기능이고

두번째는, 문서가 열릴때 앞서 말한, DB에 저장된 카드 데이터들을 모두 다 불러와서 카드 형식으로 데이터를 조회하는 리스팅 기능이다.

강사님이 이를 정리해주셔서 아래와 같은 표로 만들어주셨다.

포스팅API - 카드 생성 (Create)

A. 요청 정보

  • 요청 URL= /memo , 요청 방식 = POST
  • 요청 데이터 : URL(url_give), 코멘트(comment_give)

B. 서버가 제공할 기능

  • URL의 meta태그 정보를 바탕으로 제목, 설명, 이미지URL 스크래핑
  • (제목, 설명, URL, 이미지URL, 코멘트) 정보를 모두 DB에 저장

C. 응답 데이터

  • API가 정상적으로 작동하는지 클라이언트에게 알려주기 위해서 성공 메시지 보내기
  • (JSON 형식) 'result'= 'success'

리스팅API - 저장된 카드 보여주기 (Read)

A. 요청 정보

  • 요청 URL= /memo , 요청 방식 = GET
  • 요청 데이터 : 없음

B. 서버가 제공할 기능

  • DB에 저장돼있는 모든 (제목, 설명, URL, 이미지URL, 코멘트) 정보를 가져오기

C. 응답 데이터

  • 아티클(기사)들의 정보(제목, 설명, URL, 이미지URL, 코멘트) → 카드 만들어서 붙이기
  • (JSON 형식) 'articles': 아티클 정보

일단 이런 식으로 API를 설계했지만, 포스팅API에서 보다시피 크롤링을 해와야하는데 html 어디 부분을 크롤링 해와야하는지 처음에는 감이 안 잡힐수도 있다. 그래서 강사님은 이번에는 강사님이 친절히 알려주시지만, 이런 부분은 따로 파일을 하나 만들고 직접 스스로 테스트해보면서 알아봐야한다고 하시면서 이렇게 실험을 해보는 작업을 조각 기능 구현하기라고 하셨다.

그러면서 강사님은 이번에 우리는 크롤링을 메타 태그(meta tag) 대상으로 하실거라면서 메타 태그는 head 태그 중간에 들어가는, 눈으로 보이는것(body) 이외의 사이트 속성을 설명해주는 태그라고 하셨다.

흔히 구글 검색이나 카톡으로 사이트 링크를 보냈을때 사이트 제목, 표시 이미지, 설명문이 그 예라고 하셨다.

그래서 강사님 말씀대로 일단 연습을 위해 meta_prac.py 파일을 만들고 여기에 연습을 시작했다.

그리고 저번 크롤링 연습때 배웠던것과 같이 아래 코드로 크롤링을 위한 기본 코드를 입력했다.

import requests
from bs4 import BeautifulSoup

url = 'https://movie.naver.com/movie/bi/mi/basic.nhn?code=171539'

headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
data = requests.get(url,headers=headers)

soup = BeautifulSoup(data.text, 'html.parser')

# 여기에 코딩을 해서 meta tag를 먼저 가져와보겠습니다.

그리고 크롬 개발자도구에 들어가서 보니 meta 태그의 og:title 항목이 영화 제목을 가리키고 있어서 그것의 선택자를 복사해 soup의 select_one 기능과 합쳐서 제목만 크롤링을 해보려고 했지만 출력값이 'none'으로 나왔다. 강사님이 설명하길, 우리가 크롬으로 보는 og:title의 순서와 파이썬으로 보는 og:title의 순서가 다를수도 있기에 이런 경우가 생긴다고 하셨다.

원래대로라면, 선택자의 일부분을 조금씩 바꾸면서 테스트를 해봐야하지만 이번의 경우 강사님이 한가지 방법을 알려주셨다. 바로 beautifulSoup의 기능인데 선택자를 입력하는곳에 아래와 같이 입력하면 그 해당 meta 태그와 일치하는 속성만 가져와서 변수에 넣어준다는것이었다.

og_image = soup.select_one('meta[property="og:image"]')
og_title = soup.select_one('meta[property="og:title"]')
og_description = soup.select_one('meta[property="og:description"]')

print(og_image)
print(og_title)
print(og_description)

(이미지는 og:image, 제목은 og:title, 설명은 og:description)

그러면 아래와 같이 출력된다.

근데 우리는 여기서 content 부분만 뽑아야 하기에 아래 코드로 수정하면 포스팅 API에 쓸 크롤링 데이터가 완성된다.

og_image = soup.select_one('meta[property="og:image"]')['content']
og_title = soup.select_one('meta[property="og:title"]')['content']
og_description = soup.select_one('meta[property="og:description"]')['content']

print(og_image)
print(og_title)
print(og_description)

그러면 아래와 같이 우리가 원했던 해당 영화 이미지 주소, 제목, 설명이 잘 뽑혀나온다.

이렇게 완성이 됐다 싶으면 서버 부분인 app.py에 이 코드들을 적재적소에 넣어주기만 하면 된다.

가장 미지였던 크롤링도 완성했겠다, 이제 그럼 앞서 했던 모두의책리뷰와 동일하게 뼈대부터 만들어야 했다.

아래 코드를 먼저 app.py에 입력했다.

from flask import Flask, render_template, jsonify, request
app = Flask(__name__)

import requests
from bs4 import BeautifulSoup

from pymongo import MongoClient
client = MongoClient('localhost', 로컬포트숫자)
db = client.dbsparta

## HTML을 주는 부분
@app.route('/')
def home():
   return render_template('index.html')

@app.route('/memo', methods=['GET'])
def listing():
    sample_receive = request.args.get('sample_give')
    print(sample_receive)
    return jsonify({'msg':'GET 연결되었습니다!'})

## API 역할을 하는 부분
@app.route('/memo', methods=['POST'])
def saving():
    sample_receive = request.form['sample_give']
    print(sample_receive)
    return jsonify({'msg':'POST 연결되었습니다!'})

if __name__ == '__main__':
   app.run('0.0.0.0',port=포트숫자,debug=True)

그리고 templates 폴더에 index.html 파일을 만들고 아래와 같은 코드를 입력했다.

<!Doctype html>
<html lang="ko">

    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
              integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
              crossorigin="anonymous">

        <!-- JS -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
                integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
                crossorigin="anonymous"></script>

        <!-- 구글폰트 -->
        <link href="https://fonts.googleapis.com/css?family=Stylish&display=swap" rel="stylesheet">


        <title>스파르타코딩클럽 | 나홀로 메모장</title>

        <!-- style -->
        <style type="text/css">
            * {
                font-family: "Stylish", sans-serif;
            }

            .wrap {
                width: 900px;
                margin: auto;
            }

            .comment {
                color: blue;
                font-weight: bold;
            }

            #post-box {
                width: 500px;
                margin: 20px auto;
                padding: 50px;
                border: black solid;
                border-radius: 5px;
            }
        </style>
        <script>
            $(document).ready(function () {
                showArticles();
            });
            
            function openClose() {
                if ($("#post-box").css("display") == "block") {
                    $("#post-box").hide();
                    $("#btn-post-box").text("포스팅 박스 열기");
                } else {
                    $("#post-box").show();
                    $("#btn-post-box").text("포스팅 박스 닫기");
                }
            }

            function postArticle() {
                $.ajax({
                    type: "POST",
                    url: "/memo",
                    data: {sample_give:'샘플데이터'},
                    success: function (response) { // 성공하면
                        alert(response["msg"]);
                    }
                })
            }

            function showArticles() {
                $.ajax({
                    type: "GET",
                    url: "/memo?sample_give=샘플데이터",
                    data: {},
                    success: function (response) {
                        alert(response["msg"]);
                    }
                })
            }
        </script>

    </head>

    <body>
        <div class="wrap">
            <div class="jumbotron">
                <h1 class="display-4">나홀로 링크 메모장!</h1>
                <p class="lead">중요한 링크를 저장해두고, 나중에 볼 수 있는 공간입니다</p>
                <hr class="my-4">
                <p class="lead">
                    <button onclick="openClose()" id="btn-post-box" type="button" class="btn btn-primary">포스팅 박스 열기
                    </button>
                </p>
            </div>
            <div id="post-box" class="form-post" style="display:none">
                <div>
                    <div class="form-group">
                        <label for="post-url">아티클 URL</label>
                        <input id="post-url" class="form-control" placeholder="">
                    </div>
                    <div class="form-group">
                        <label for="post-comment">간단 코멘트</label>
                        <textarea id="post-comment" class="form-control" rows="2"></textarea>
                    </div>
                    <button type="button" class="btn btn-primary" onclick="postArticle()">기사저장</button>
                </div>
            </div>
            <div id="cards-box" class="card-columns">
                <div class="card">
                    <img class="card-img-top"
                         src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
                         alt="Card image cap">
                    <div class="card-body">
                        <a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
                        <p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
                        <p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
                    </div>
                </div>
                <div class="card">
                    <img class="card-img-top"
                         src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
                         alt="Card image cap">
                    <div class="card-body">
                        <a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
                        <p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
                        <p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
                    </div>
                </div>
                <div class="card">
                    <img class="card-img-top"
                         src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
                         alt="Card image cap">
                    <div class="card-body">
                        <a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
                        <p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
                        <p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
                    </div>
                </div>
                <div class="card">
                    <img class="card-img-top"
                         src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
                         alt="Card image cap">
                    <div class="card-body">
                        <a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
                        <p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
                        <p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
                    </div>
                </div>
                <div class="card">
                    <img class="card-img-top"
                         src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
                         alt="Card image cap">
                    <div class="card-body">
                        <a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
                        <p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
                        <p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
                    </div>
                </div>
                <div class="card">
                    <img class="card-img-top"
                         src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
                         alt="Card image cap">
                    <div class="card-body">
                        <a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
                        <p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
                        <p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
                    </div>
                </div>
            </div>
        </div>
    </body>

</html>

그러면 아래 그림과 같이 기본 뼈대로도 잘 작동하는 모습을 볼 수 있다.

그리고 실제 API를 코드로 쳐 구현할 차례여서 먼저 포스팅 API를 만들어보았다.

서버 코드 app.py의 포스팅 api 부분은 아래와 같았다.

@app.route('/memo', methods=['POST'])
def saving():
    sample_receive = request.form['sample_give']
    print(sample_receive)
    return jsonify({'msg':'POST 연결되었습니다!'})

그리고 클라이언트 코드 index.html의 포스팅 api 부분은 아래와 같았다.

function postArticle() {
  $.ajax({
    type: "POST",
    url: "/memo",
    data: {sample_give:'샘플데이터'},
    success: function (response) { // 성공하면
      alert(response['msg']);
    }
  })
}

<button type="button" class="btn btn-primary" onclick="postArticle()">기사저장</button>

서버에서는 sample_give를 받아 그걸 sample_receive로 넘겨주고 print 기능으로 출력, 마지막엔 msg : POST 연결되었습니다! 문장을 출력해주고

클라이언트에서는 postArticle 함수로 지정되어있는데 내용은 '샘플데이터'가 sample_give로 넘겨지고 모든게 다 절차대로 이뤄지면 response 값으로 msg를 받아 이걸 또 alert 기능을 이용해 경고창으로 출력하게 끔 설정되어있으며, 이 모든게 기사저장 버튼에 onclick 함수로 실행되어있게 설정되어있다.

이 상태에서 기사저장 버튼을 눌러 post api를 실행해보면 아래 그림과 같이 나온다.

그러면 이제 아까 우리가 목표했던 api 설계에 맞춰 post API 코드부분을 수정해야한다. 먼저 서버 부분으로 아래와 같은 설계 방향과 코드로 app.py을 수정했다.

API 는 약속이라고 했습니다. 위에 미리 설계해 둔 API 정보를 보고 만들어보죠!

메모를 작성하기 위해 서버가 전달받아야하는 정보는 다음 두 가지 입니다.

  • URL(url_give)
  • 코멘트(comment_give)

그리고 URL를 meta tag를 스크래핑해서 아래 데이터를 저장(Create)합니다.

  • URL(url)
  • 제목(title)
  • 설명(desc)
  • 이미지URL(image)
  • 코멘트(comment)

따라서 서버 로직은 다음 단계로 구성되어야 합니다.
1. 클라이언트로부터 데이터를 받기.
2. meta tag를 스크래핑하기
3. mongoDB에 데이터를 넣기

@app.route('/memo', methods=['POST'])
def saving():
    url_receive = request.form['url_give']
    comment_receive = request.form['comment_give']

    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
    data = requests.get(url_receive, headers=headers)

    soup = BeautifulSoup(data.text, 'html.parser')

    title = soup.select_one('meta[property="og:title"]')['content']
    image = soup.select_one('meta[property="og:image"]')['content']
    desc = soup.select_one('meta[property="og:description"]')['content']

    doc = {
        'title':title,
        'image':image,
        'desc':desc,
        'url':url_receive,
        'comment':comment_receive
    }

    db.articles.insert_one(doc)

    return jsonify({'msg':'저장이 완료되었습니다!'})

클라이언트 index.html 포스팅박스에서 인풋박스에 입력된 url,commnet를 각각 url_give, comment_give로 선언하고 이를 url_receive, comment_receive 변수에 담아 크롤링할 초기 데이터 data에 담고 또 그걸 soup에 html 형식으로 변환해서 이 html 데이터 soup에서 우리가 아까 배운 해당 메타 태그와 일치하는것만 뽑는 코드 기능으로 title, image, desc으로 다시 담고 이제 이를 데이터베이스에 담아줘야 하기 때문에 먼저 담길 그릇인 doc의 딕셔너리 형태를 먼저 선언해주고 데이터베이스의 articels 항목에 담아지도록 db.articles.insert_one(doc)으로 일단 마지막 데이터베이스에 옮기는 서버 부분 코드까지 완성한다. 추가적으로 msg는 저장이 완료되었습니다로 변경한다. 이러면 포스팅 기능 서버 부분은 완료가 되었다.

다음은 클라이언트 부분을 맞춰서 수정해야한다. 아래와 같은 방향과 코드로 index.html을 수정한다.

API 는 약속이라고 했습니다. API를 사용할 클라이언트를 만들어보죠!

메모를 작성하기 위해 서버에게 주어야하는 정보는 다음 두 가지 입니다.

  • URL (url_give) : meta tag를 가져올 url
  • comment (comment_give) : 유저가 입력한 코멘트

따라서 클라이언트 로직은 다음 단계로 구성되어야 합니다.
1. 유저가 입력한 데이터를 #post-url과 #post-comment에서 가져오기
2. /memo에 POST 방식으로 메모 생성 요청하기
3. 성공 시 페이지 새로고침하기

function postArticle() {
      let url = $('#post-url').val()
      let comment = $('#post-comment').val()

      $.ajax({
          type: "POST",
          url: "/memo",
          data: {url_give:url, comment_give:comment},
          success: function (response) { // 성공하면
              alert(response["msg"]);
              window.location.reload()
          }
      })
  }

클라이언트에서 post api 함수를 담당하고 있는 부분은 postArticle 함수로 url은 인풋박스 post-url id에서, comment는 인풋박스 post-comment에서 값을 받아와서 넣어지고 ajax 콜에선 POST 요청으로 data는 방금 받았던 url과 comment의 값을 url_give, comment_give에 다시 넣어 아까 만들었던 서버 post api 부분과 매칭이 되게 해주었다. 이렇게 해서 모든 절차가 잘 돌아가면 msg를 경고창으로 뜨게끔 놔둔뒤 끝나면 창이 새로고침이 되도록window.location.reload() 까지 넣어주면 완성이 된다.

그러면 아래 사진과 같이 나홀로메모장에서 포스팅 기능이 되고 데이터 베이스에도 정상적으로 저장이 된다.

그럼 이제 마지막 리스팅 기능, 나홀로메모장 데이터베이스에 저장된 모든 데이터들을 조회하는 GET 기능을 설계대로 실제 구현해야했다.

본격적으로 하기 전에 강사님이랑 리스팅 API 안에 어떤 기능을 구현해야하는지 아래 내용으로
다시 한번 더 복습했다.

1) 포스팅API - 카드 생성 (Create) : 클라이언트에서 받은 url, comment를 이용해서 페이지 정보를 찾고 저장하기
2) 리스팅API - 저장된 카드 보여주기 (Read)

일단 app.py와 index.html에서 GET 요청 부분만을 찾아 서버-클라이언트 흐름상 기본 뼈대가 잘 작동하는지 확인했다.

@app.route('/memo', methods=['GET'])
def listing():
    sample_receive = request.args.get('sample_give')
    print(sample_receive)
    return jsonify({'msg':'GET 연결되었습니다!'})
$(document).ready(function () {
 	showArticles();
});

function showArticles() {
  $.ajax({
    type: "GET",
    url: "/memo?sample_give=샘플데이터",
    data: {},
    success: function (response) {
      if (response["result"] == "success") {
        alert(response["msg"]);
      }
    }
  })
}

app.py 서버 부분에서 sample_give, sample_receive간 절차가 잘 되면 json 형식으로 msg : GET 연결되었습니다! 문자열을 출력하고 index.html 클라이언트 부분에서는 showArticles 함수가 Get 요청을 담당하고 있었고 이 함수는 document.ready로 문서가 로딩완료 되자마자 실행되는 구조였고 함수내 절차가 잘 되면 서버에서 넘어온 response를 받아 그 중 msg만을 alert 경고창으로 출력하고 있는 순서였다.

그래서 아래 사진과 같이 나홀로메모장 문서가 열리면 'GET 연결되었습니다!'가 담긴 경고창이 열렸다.

일단 앞서 했던것과 똑같이 서버 부분을 먼저 강사님 지시대로 만들어보았다. 아래 정리된 흐름 방향을 참고해서 다음에 나오는 코드로 app.py 안 GET 요청 코드를 수정했다.

API 는 약속이라고 했습니다. 위에 미리 설계해 둔 API 정보를 보고 만들어보죠!

메모를 보여주기 위해 서버가 추가로 전달받아야하는 정보는 없습니다. 조건없이 모든 메모를 보여줄 꺼니까요!

따라서 서버 로직은 다음 단계로 구성되어야 합니다.
1. mongoDB에서 _id 값을 제외한 모든 데이터 조회해오기 (Read)
2. articles라는 키 값으로 articles 정보 보내주기

@app.route('/memo', methods=['GET'])
def listing():
    articles = list(db.articles.find({}, {'_id': False}))
    return jsonify({'all_articles':articles})

db.articles.find({},{'_id':False})로 해당되는 모든 데이터를 찾아서 list 기능으로 리스트 형태의 데이터로 한꺼번에 묶고 이걸 articles로 넘겨준뒤, json 형식으로 articles 값을 all_articles에 최종적으로 넘겨준다.

그리고 그 다음은 당연히 클라이언트 부분이다. 아래 정리된 흐름 방향을 참고해서 다음에 나오는 코드로 index.html 안 GET 요청 코드를 수정했다.

API 는 약속이라고 했습니다. API를 사용할 클라이언트를 만들어보죠!

메모를 작성하기 위해 서버에게 주어야하는 정보는 없습니다. 조건없이 모든 메모를 가져오기 때문입니다.

따라서 클라이언트 로직은 다음 단계로 구성되어야 합니다.
1. /memo에 GET 방식으로 메모 정보 요청하고 articles로 메모 정보 받기
2. , makeCard 함수를 이용해서 카드 HTML 붙이기
(→ 2주차 Ajax 연습과 같습니다!)

function showArticles() {
    $.ajax({
        type: "GET",
        url: "/memo",
        data: {},
        success: function (response) {
            let articles = response['all_articles']
            for (let i = 0; i < articles.length; i++) {
                let title = articles[i]['title']
                let image = articles[i]['image']
                let url = articles[i]['url']
                let desc = articles[i]['desc']
                let comment = articles[i]['comment']

                let temp_html = `<div class="card">
                                    <img class="card-img-top"
                                         src="${image}"
                                         alt="Card image cap">
                                    <div class="card-body">
                                        <a target="_blank" href="${url}" class="card-title">${title}</a>
                                        <p class="card-text">${desc}</p>
                                        <p class="card-text comment">${comment}</p>
                                    </div>
                                </div>`
                $('#cards-box').append(temp_html)
            }
        }
    })
}

그렇게 서버에서 받은 response 중 all_artilces를 골라 이걸 articles에 다시 담고 이걸 for 반복문을 돌리되, articles의 title,image,url,desc,comment를 각각 선언해주고 새로 카드로 추가할 html 내용이 class=card 전체 부분에 해당하는 것이었으므로 그 구조를 그대로 복사해 안에 title,image,url,desc,comment 내용만 for 반복문에 맞게 변경한뒤 마지막 id cards-box에 append 기능으로 카드가 추가로 붙여지게 끔 마무리를 하면 되었다.

그러면 아래 그림과 같이 문서가 로딩되자마자 데이터베이스에 있는 영화 데이터들을 불러와 카드로 추가되어 붙는 GET 요청이 잘 처리되는것을 알 수 있다.


4. 4주차 숙제

4주차 숙제는 위 사진처럼 우리가 2주차때 완성했던 원페이지 쇼핑몰 페이지에 POST, GET 요청 기능을 넣어 주문하기 버튼을 누르면 주문자 이름, 주문수량, 주소, 전화번호를 데이터베이스에 저장하고 아래 주문목록에 추가하고 페이지가 로딩될때마다 아래 전체 주문내역이 자동으로 보이도록 하는 기능을 직접 구현하는것이었다.

나는 직접 천천히 강사님이 가르쳐주신걸 떠올리면서 새 프로젝트 설치부터 필요한 라이브러리 설치(flask,pymongo) 그리고 templates static 폴더 생성, 필요한 API 기능 다시 떠올리고 설계하기 그리고 마지막 직접 코드 치며 GET/POST API 기능 구현까지 다 스스로 해보았다.

먼저 서버 부분 app.py를 아래 코드로 만들었다.

from flask import Flask, render_template, jsonify, request

app = Flask(__name__)

from pymongo import MongoClient

client = MongoClient('localhost', 로컬포트숫자)
db = client.dbhomework


## HTML 화면 보여주기
@app.route('/')
def homework():
    return render_template('index.html')


# 주문하기(POST) API
@app.route('/order', methods=['POST'])
def save_order():
    name_receive = request.form['name_give']
    quantity_receive = request.form['quantity_give']
    address_receive = request.form['address_give']
    phone_number_receive = request.form['phone_number_give']

    doc = {
        'name': name_receive,
        'quantity': quantity_receive,
        'address': address_receive,
        'phone_number': phone_number_receive
    }

    db.orders.insert_one(doc)

    return jsonify({'result': 'success', 'msg': '주문이 완료되었습니다!'})


# 주문 목록보기(Read) API
@app.route('/order', methods=['GET'])
def view_orders():
    orders = list(db.orders.find({}, {'_id': False}))
    return jsonify({'result': 'success', 'orders': orders})


if __name__ == '__main__':
    app.run('0.0.0.0', port=포트숫자, debug=True)

그 다음 클라이언트 부분인 index.html을 원래 만들었던 원페이지 쇼핑몰 코드 기반에서 API 기능 추가와 필요한, 자잘한 id 지정까지 해주면서 아래와 같이 완성했다.

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

<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
          integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
            integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
            crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
            integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
            crossorigin="anonymous"></script>

    <title>스파르타코딩클럽 | 부트스트랩 연습하기</title>

    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Gowun+Batang&display=swap" rel="stylesheet">

    <style>
        .item_image {
            height: 500px;
            width: 500px;
            background-image: url("https://ae01.alicdn.com/kf/HTB1ywReyY9YBuNjy0Fgq6AxcXXaO/4-8-JDH99.jpg_Q90.jpg_.webp");
            background-position: center;
            background-size: cover;
        }

        .price {
            font-size: 17px;
        }

        .item_desc {
            width: 500px;
            margin-top: 15px;
            margin-bottom: 15px;
        }

        .item_order {
            width: 500px;

        }

        .btn_order {
            margin: auto;
            width: 120px;
            display: block;
        }

        .wrap {
            width: 500px;
            margin: auto;
        }

        * {
            font-family: 'Gowun Batang', serif;
        }

        .rate {
            color: blue;
        }
    </style>

    <script>
        $(document).ready(function () {
            get_rate();
            listing();
        });

        function order() {
            let name = $('#order-name').val();
            let quantity = $('#order-quantity').val();
            let address = $('#order-address').val();
            let phone_number = $('#order-phone_number').val();

            $.ajax({
                type: "POST",
                url: "/order",
                data: {
                    name_give: name,
                    quantity_give: quantity,
                    address_give: address,
                    phone_number_give: phone_number
                },
                success: function (response) {
                    if (response["result"] == "success") {
                        alert(response["msg"]);
                        window.location.reload();
                    }
                }
            })
        }

        function listing() {
            $.ajax({
                type: "GET",
                url: "/order",
                data: {},
                success: function (response) {
                    if (response["result"] == "success") {
                        let orders = response['orders']
                        for (let i = 0; i < orders.length; i++) {
                            let name = orders[i]['name'];
                            let quantity = orders[i]['quantity'];
                            let address = orders[i]['address'];
                            let phone_number = orders[i]['phone_number'];

                            let temp_html = `<tr>
                                                <th scope="row">${name}</th>
                                                <td>${quantity}</td>
                                                <td>${address}</td>
                                                <td>${phone_number}</td>
                                            </tr>`
                                $('#orders-box').append(temp_html)
                        }
                    }
                }
            })
        }

        function get_rate() {
            $.ajax({
                type: "GET",
                url: "http://spartacodingclub.shop/sparta_api/rate",
                data: {},
                success: function (response) {
                    let current_rate = response['rate']
                    $('#current-rate').text(current_rate)
                }

            })
        }

    </script>
</head>

<body>
<div class="wrap">
    <div class="item_image"></div>
    <div class="item_desc">
        <h1>기름 램프를 팝니다 <span class="price">가격: 3000원/개</span></h1>
        <p>이 램프는 사실 소원을 들어주는 램프입니다. 기름을 넣고 불을 키면 어떤 소원이든지 들어드려요. 게다가 좋은 아로마 향도 난답니다.</p>
        <p class="rate">달러-원 환율: <span id="current-rate">1219.5</span></p>
    </div>
    <div class="item_order">
        <div class="input-group mb-3">
            <div class="input-group-prepend">
                <span class="input-group-text" id="inputGroup-sizing-default">주문자이름</span>
            </div>
            <input id="order-name" type="text" class="form-control" aria-label="Default"
                   aria-describedby="inputGroup-sizing-default">
        </div>

        <div class="input-group mb-3">
            <div class="input-group-prepend">
                <label class="input-group-text" for="inputGroupSelect01">수량</label>
            </div>
            <select class="custom-select" id="order-quantity">
                <option selected>-- 수량을 선택하세요 --</option>
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
                <option value="3">4</option>
                <option value="3">5</option>
                <option value="3">6</option>
                <option value="3">7</option>
                <option value="3">8</option>
                <option value="3">9</option>
                <option value="3">10</option>
            </select>
        </div>

        <div class="input-group mb-3">
            <div class="input-group-prepend">
                <span class="input-group-text" id="inputGroup-sizing-default">주소</span>
            </div>
            <input id="order-address" type="text" class="form-control" aria-label="Default"
                   aria-describedby="inputGroup-sizing-default">
        </div>

        <div class="input-group mb-3">
            <div class="input-group-prepend">
                <span class="input-group-text" id="inputGroup-sizing-default">전화번호</span>
            </div>
            <input id="order-phone_number" type="text" class="form-control" aria-label="Default"
                   aria-describedby="inputGroup-sizing-default">
        </div>

        <button type="button" class="btn_order btn-primary" onclick="order()">주문하기</button>
    </div>
    <table class="table">
        <thead>
        <tr>
            <th scope="col">이름</th>
            <th scope="col">수량</th>
            <th scope="col">주소</th>
            <th scope="col">전화번호</th>
        </tr>
        </thead>
        <tbody id="orders-box">
        </tbody>
    </table>
</div>
</body>

</html>

그렇게 해서 완성으로 만든 4주차 숙제는 아래 그림과 같이 잘 작동하였다.

0개의 댓글