본 내용은 스파르타 코딩클럽 웹개발 플러스 2주차 강의의 정리본 입니다 :D
본 수업을 듣고싶으시면 아래 링크를 클릭하여 친구추천 이벤트로 5만원을 할인 받으실 수 있습니다 !!
sparta coding club 친구 링크 : https://spartacodingclub.kr/?f_name=%ED%99%8D%EC%9A%B0%EC%A7%84&f_uid=6099457322dccf3b2ac8b84c
(위 링크에서 강의 결제시 5만원 할인이 적용됩니다!)
2주차 프로젝트는 owlbot api를 이용한 나만의 단어장 만들기가 프로젝트였다.
1) 페이지 간 이동하기
메인 페이지에서 링크를 클릭하면 상세 페이지로 가고, 상세 페이지에서 다시 메인으로 갈 수 있게 하려면 어떻게 해야할까요?
우선 파이참으로 이번주의 프로젝트를 준비해야겠죠!
File > New Project...에 가서 project02 폴더 열기
Project Interpreter에서 가상환경에 필요한 패키지 설치하기(flask
, requests
, pymongo
)
project02 폴더 안에 templates, static 폴더 만들기
app.py, index.html, detail.html 파일 만들기
[코드스니펫] - app.py 시작코드
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def main():
return render_template("index.html")
@app.route('/detail')
def detail():
return render_template("detail.html")
if __name__ == '__main__':
app.run('0.0.0.0', port=5000, debug=True)
페이지 연결하기
상세 페이지로 가는 하이퍼링크는 이렇게 만듭니다.
<a href="/detail">상세 페이지로 가기</a>
메인 페이지로 돌아가는 버튼은 이렇게 만들 수 있겠죠!
// script 태그 안에 정의하기
function to_main() {
window.location.href = "/"
}
<!-- 버튼에 함수 연결하기 -->
<button onclick="to_main()">메인으로 돌아가기</button>
짧은 코드는 onclick
에 바로 넣을 수 있습니다.
<button onclick='window.location.href = "/"'>메인으로 돌아가기</button>
2) Jinja2 템플릿 언어 이용하기
서버에서 name
이라는 이름으로 값을 보내줍니다.
@app.route('/')
def main():
myname = "sparta"
return render_template("index.html", name=myname)
html 파일에서 이 값이 들어갈 자리를 표시해줍니다.
```html
<h3>Hello, {{ name }}!</h3>
```
파이참 Settings(맥은 Preferences) > Languages & Frameworks > Template Languages에서 템플릿 언어를 Jinja2로 설정해주면 자동완성과 하이라이팅 기능을 사용할 수 있어요!
이부분 응용하기!
3) Jinja2 템플릿 언어 이용하기 응용편
Jinja를 본격적으로 쓰기에 앞서, 원래 ajax 요청을 보내 html을 완성할 때는 어떻게 했었는지 살펴봅시다. 서울시 Open API에서 정보를 받아와 현재 미세먼지 수치가 50 이상인 구만 페이지에 나타내볼까요?
[코드스니펫] - ajax 요청 준비
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script>
$(document).ready(function () {
get_list()
})
function get_list() {
$.ajax({
type: "GET",
url: "http://openapi.seoul.go.kr:8088/6d4d776b466c656533356a4b4b5872/json/RealtimeCityAir/1/99",
data: {},
success: function (response) {
let rows = response["RealtimeCityAir"]["row"];
console.log(rows)
}
})
}
</script>
미세먼지 정보가 들어갈 ul 태그 만들기
<ul id="gu-list">
</ul>
각 구에 대해서 구 이름과 미세먼지 수치를 변수에 저장하기
for (let i=0;i<rows.length;i++) {
let gu_name = rows[i]["MSRSTE_NM"]
let gu_mise = rows[i]["IDEX_MVL"]
console.log(gu_name, gu_mise)
}
미세먼지 수치가 50 이상일 때만 태그 추가하기
if (gu_mise >= 50) {
let html_temp = `<li>${gu_name}: ${gu_mise}</li>`
$("#gu-list").append(html_temp)
}
이번에는 같은 일을 jinja2로 해보겠습니다.
[코드스니펫] - requests 요청 보내기
r = requests.get('http://openapi.seoul.go.kr:8088/6d4d776b466c656533356a4b4b5872/json/RealtimeCityAir/1/99')
response = r.json()
rows = response['RealtimeCityAir']['row']
렌더링할 html에 미세먼지 정보 보내기
return render_template("index.html", name=name, rows=rows)
첫번째 구의 정보를 태그로 만들기
<li>{{ rows[0].MSRSTE_NM }}: {{ rows[0].IDEX_MVL }}</li>
변수에 저장하기
{% set gu_name = rows[0].MSRSTE_NM %}
{% set gu_mise = rows[0].IDEX_MVL %}
<li>{{ gu_name }}: {{ gu_mise }}</li>
모든 구에 대해서 태그 만들기
{% for row in rows %}
{% set gu_name = row.MSRSTE_NM %}
{% set gu_mise = row.IDEX_MVL %}
<li>{{ gu_name }}: {{ gu_mise }}</li>
{% endfor %}
미세먼지 수치가 50 이상일 때만 태그 만들기
{% if gu_mise >= 50 %}
<li>{{ gu_name }}: {{ gu_mise }}</li>
{% endif %}
그 외에도, head 태그 안의 내용(title 태그 등)을 바꿀 때도 쓸 수 있고, 다른 html 문서를 통째로 가져와 템플릿으로 사용할 수도 있습니다.
4) URL의 일부를 변수로 받기
브라우저에 HTML을 띄우는 것은 GET 요청이기 때문에, 주소 뒤에 ?
를 붙여 파라미터를 넘겨줄 수 있습니다.
브라우저에 해당 API로 요청 보내기
http://localhost:5000/detail?word_give=hello
서버에서 파라미터 값을 받아 HTML으로 넘겨주기
@app.route('/detail')
def detail():
word_receive = request.args.get("word_give")
return render_template("detail.html", word=word_receive)
HTML에서 word
라는 변수에 저장된 값 나타내기
받은 단어는 {{ word }}
플라스크 프레임워크에서는 URL의 일부를 변수로 받는 방법도 사용할 수 있습니다.
@app.route('/detail/<keyword>')
def detail(keyword):
return render_template("detail.html", word=keyword)
9) 문제 분석 - 완성작부터 보기: 나만의 단어장
[코드스니펫] - 나만의 단어장 보러가기
http://spartacodingclub.shop/wp/vocab
10) API 설계하기
필요한 기능들을 생각해봅시다. 각 페이지에서는 어떤 일이 일어나야 하나요?
메인 페이지
단어 검색
단어가 단어장에 이미 있는 단어인지 검색 → 있으면 하이라이트, 없으면 상세 페이지로 이동
단어장에 있는 단어를 클릭했을 때 상세 페이지로 이동
상세 페이지
1. 단어 저장 또는 삭제
- 단어가 이미 존재하면 삭제 버튼, 아니면 저장 버튼 노출
- 저장 버튼을 누르면 DB에 저장하고 삭제 버튼으로 바뀜
- 삭제 버튼을 누르면 DB에서 삭제하고 메인 페이지로 이동
2. 예문 저장과 삭제
- 저장된 단어의 경우 예문 칸이 보여지게 하기
- 예문을 저장하면 목록 맨 아래에 추가
- 예문에 단어가 포함되지 않으면 얼럿 띄우기
- 예문을 선택해서 삭제할 수 있음
11) 프로젝트 준비 - app.py 준비하기
[코드스니펫] - 나만의 단어장 app.py 시작코드
```python
from flask import Flask, render_template, request, jsonify, redirect, url_for
from pymongo import MongoClient
import requests
app = Flask(__name__)
client = MongoClient('내AWS아이피', 27017, username="아이디", password="비밀번호")
db = client.dbsparta_plus_week2
@app.route('/')
def main():
# DB에서 저장된 단어 찾아서 HTML에 나타내기
return render_template("index.html")
@app.route('/detail/<keyword>')
def detail(keyword):
# API에서 단어 뜻 찾아서 결과 보내기
return render_template("detail.html", word=keyword)
@app.route('/api/save_word', methods=['POST'])
def save_word():
# 단어 저장하기
return jsonify({'result': 'success', 'msg': '단어 저장'})
@app.route('/api/delete_word', methods=['POST'])
def delete_word():
# 단어 삭제하기
return jsonify({'result': 'success', 'msg': '단어 삭제'})
if __name__ == '__main__':
app.run('0.0.0.0', port=5000, debug=True)
```
로컬에서 개발할 때도 AWS 서버에 있는 MongoDB를 바로 연결해서 코딩하면 나중에 배포할 때 DB를 복사해 옮길 필요가 없어 편합니다 😉
12) 프로젝트 준비 - index.html, detail.html 준비하기
13) 프로젝트 준비 - 배너 이미지 준비하기
25) 단어 검색 기능 만들기
단어를 검색했을 때 이미 저장된 단어인지 알기 위해서 있는 단어 리스트를 만듭니다.
let words = {{ words|tojson }};
let word_list = [];
for (let i = 0; i < words.length; i++) {
word_list.push(words[i]["word"])
}
단어를 검색했을 때 단어 리스트에 있는 경우에는 해당 행을 하이라이트하고, 없는 단어일 때는 단어 상세페이지로 넘어가는 기능을 만듭니다.
function find_word() {
let word = $("#input-word").val().toLowerCase();
if (word == "") {
// 빈 문자열이면 얼럿
alert("please write something first :)")
return
}
if (word_list.includes(word)) {
// 리스트에 있으면 하이라이트
$(`#word-${word}`).addClass('highlight').siblings().removeClass('highlight');
$(`#word-${word}`)[0].scrollIntoView();
} else {
// 리스트에 없으면 상세 페이지로
window.location.href = `/detail/${word}?status_give=new`
}
}
하이라이트된 행은 다음과 같은 CSS로 나타내줍니다.
tr.highlight > td {
background-color: #e8344e;
color: white;
}
tr.highlight a {
color: white;
}
26) 단어가 존재하지 않을 때 기능 만들기
27) og태그, favicon 넣기
28) 전체 완성 코드
app.py
from flask import Flask, render_template, request, jsonify, redirect, url_for
from pymongo import MongoClient
import requests
app = Flask(__name__)
client = MongoClient('mongodb://아이디:비밀번호@내 아이피', 27017)
db = client.dbsparta_plus_week2
@app.route('/')
def main():
msg = request.args.get("msg")
# DB에서 저장된 단어 찾아서 HTML에 나타내기
words = list(db.words.find({}, {"_id": False}))
return render_template("index.html", words=words, msg=msg)
@app.route('/detail/<keyword>')
def detail(keyword):
# API에서 단어 뜻 찾아서 결과 보내기
status_receive = request.args.get("status_give", "old")
r = requests.get(f"https://owlbot.info/api/v4/dictionary/{keyword}",
headers={"Authorization": "Token [내 토큰]"})
if r.status_code != 200:
return redirect(url_for("main", msg="Word not found in dictionary; Try another word"))
result = r.json()
print(result)
return render_template("detail.html", word=keyword, result=result, status=status_receive)
@app.route('/api/save_word', methods=['POST'])
def save_word():
# 단어 저장하기
word_receive = request.form['word_give']
definition_receive = request.form['definition_give']
doc = {"word": word_receive, "definition": definition_receive}
db.words.insert_one(doc)
return jsonify({'result': 'success', 'msg': f'word "{word_receive}" saved'})
@app.route('/api/delete_word', methods=['POST'])
def delete_word():
# 단어 삭제하기
word_receive = request.form['word_give']
db.words.delete_one({"word": word_receive})
return jsonify({'result': 'success', 'msg': f'word "{word_receive}" deleted'})
if __name__ == '__main__':
app.run('0.0.0.0', port=5000, debug=True)
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Sparta Vocabulary Notebook</title>
<meta property="og:title" content="Sparta Vocabulary Notebook"/>
<meta property="og:description" content="mini project for Web Plus"/>
<meta property="og:image" content="{{ url_for('static', filename='logo_red.png') }}"/>
<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">
<!-- 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">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href='{{ url_for("static", filename="mystyle.css") }}' rel="stylesheet">
<!-- 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>
<style>
.search-box {
width: 70%;
margin: 50px auto;
max-width: 700px;
}
.table {
width: 80%;
max-width: 800px;
margin: auto;
table-layout: fixed;
}
.table th {
border-top-style: none;
}
td {
background-color: white;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
td > a, a:visited, a:hover, a:active {
color: black;
}
tr.highlight > td {
background-color: #e8344e;
color: white;
}
tr.highlight a {
color: white;
}
thead:first-child tr:first-child th:first-child {
border-radius: 10px 0 0 0;
}
thead:first-child tr:first-child th:last-child {
border-radius: 0 10px 0 0;
}
tbody:last-child tr:last-child td:first-child {
border-radius: 0 0 0 10px;
}
tbody:last-child tr:last-child td:last-child {
border-radius: 0 0 10px 0;
}
</style>
<script>
{% if msg %}
alert("{{ msg }}")
{% endif %}
let words = {{ words|tojson }};
let word_list = [];
for (let i = 0; i < words.length; i++) {
word_list.push(words[i]["word"])
}
function find_word() {
let word = $("#input-word").val().toLowerCase();
if (word == "") {
alert("please write something first :)")
return
}
if (word_list.includes(word)) {
$(`#word-${word}`).addClass('highlight').siblings().removeClass('highlight');
$(`#word-${word}`).get(0).scrollIntoView();
} else {
window.location.href = `/detail/${word}?status_give=new`
}
}
</script>
</head>
<body>
<div class="wrap">
<div class="banner" onclick="window.location.href = '/'">
</div>
<div class="search-box d-flex justify-content-center">
<input id="input-word" class="form-control" style="margin-right: 0.5rem">
<button class="btn btn-light" onclick="find_word()"><i class="fa fa-search"></i></button>
</div>
<table class="table">
<thead class="thead-light">
<tr>
<th scope="col" style="width:30%">WORD</th>
<th scope="col">MEANING</th>
</tr>
</thead>
<tbody id="tbody-box">
{% for word in words %}
<tr id="word-{{ word.word }}">
<td><a href="/detail/{{ word.word }}?status_give=old">{{ word.word }}</a></td>
<td>{{ word.definition|safe }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>
detail.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Sparta Vocabulary Notebook</title>
<meta property="og:title" content="Sparta Vocabulary Notebook"/>
<meta property="og:description" content="mini project for Web Plus"/>
<meta property="og:image" content="{{ url_for('static', filename='logo_red.png') }}"/>
<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">
<!-- 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">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href='{{ url_for("static", filename="mystyle.css") }}' rel="stylesheet">
<!-- 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>
<style>
span.example {
color: gray;
font-size: 14px;
}
.btn-sparta {
color: #fff;
background-color: #e8344e;
border-color: #e8344e;
}
.btn-outline-sparta {
color: #e8344e;
background-color: transparent;
background-image: none;
border-color: #e8344e;
}
</style>
<script>
let word = "{{ word }}"
$(document).ready(function () {
})
function save_word() {
$.ajax({
type: "POST",
url: `/api/save_word`,
data: {
word_give: "{{ word }}",
definition_give: "{{ result.definitions[0].definition }}"
},
success: function (response) {
alert(response["msg"])
window.location.href = "/detail/{{ word }}?status=old"
}
});
}
function delete_word() {
$.ajax({
type: "POST",
url: `/api/delete_word`,
data: {
word_give: '{{ word }}',
},
success: function (response) {
alert(response["msg"])
window.location.href = "/"
}
});
}
</script>
</head>
<body>
<div class="wrap">
<div class="banner" onclick="window.location.href = '/'">
</div>
<div class="container">
<div class="d-flex justify-content-between align-items-end">
<div>
<h1 id="word" style="display: inline;">{{ result.word }}</h1>
{% if result.pronunciation %}
<h5 id="pronunciation" style="display: inline;">/{{ result.pronunciation }}/</h5>
{% endif %}
</div>
{% if status=="new" %}
<button id="btn-save" class="btn btn-outline-sparta btn-lg" onclick="save_word()"><i
class="fa fa-floppy-o"
aria-hidden="true"></i></button>
{% else %}
<button id="btn-delete" class="btn btn-sparta btn-lg" onclick="delete_word()"><i
class="fa fa-trash-o"
aria-hidden="true"></i></button>
{% endif %}
</div>
<hr>
<div id="definitions">
{% set definitions = result.definitions %}
{% for definition in definitions %}
<div style="padding:10px">
<i>{{ definition.type }}</i>
<br>{{ definition.definition.encode('ascii', 'ignore').decode('utf-8') }}<br>
{% if definition.example %}
<span class="example">{{ definition.example.encode('ascii', 'ignore').decode('utf-8') }}</span>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
</body>
</html>
mystyle.css
.wrap {
background-color: RGBA(232, 52, 78, 0.2);
min-height: 100vh;
padding-bottom: 50px;
}
.banner {
width: 100%;
height: 200px;
background-color: white;
background-image: url('logo_red.png');
background-position: center;
background-size: contain;
background-repeat: no-repeat;
cursor: pointer;
}
.container {
width: 80%;
max-width: 800px;
margin: 30px auto;
padding: 20px;
background-color: white;
border: solid 1px gray;
border-radius: 10px;
}
각 단어에 내가 만든 예문을 저장/삭제하는 기능을 만들어봅시다!
예문을 저장/삭제하고 보여주는 기능을 만든 후 AWS 서버에 올리는 것까지가 숙제입니다! 1주차에 올려놓은 일기장은 끄고 올려야겠죠?
어디서 시작해야할지 모르겠다면? 아래 코드들을 복사해놓고 시작해보세요 😉
[코드스니펫] - 숙제 시작 코드 HTML
<div id="examples" class="container">
<h3 style="text-align: center;margin-bottom:1rem">Write your own sentences!</h3>
<ul id="example-list">
<li id="ex-0">This sentence contains the word 'word'. <a
href="javascript:delete_ex(0)">delete</a></li>
<li id="ex-1">I don't like using the MS Word program. <a
href="javascript:delete_ex(1)">delete</a></li>
</ul>
<div class="d-flex justify-content-between" style="margin-left:20px;">
<input id="new-example" class="form-control form-control-sm" style="margin-right: 0.5rem">
<button class="btn btn-outline-secondary btn-sm" onclick="add_ex()">add</button>
</div>
</div>
[코드스니펫] - 숙제 시작 코드 자바스크립트
function get_examples() {
$("#example-list").empty()
$.ajax({
type: "GET",
url: `/api/get_exs?word_give=${word}`,
data: {},
success: function (response) {
console.log(response)
}
});
}
function add_ex() {
let new_ex = $('#new-example').val();
console.log(new_ex)
$.ajax({
type: "POST",
url: `/api/save_ex`,
data: {
},
success: function (response) {
console.log(response)
get_examples()
}
});
}
function delete_ex(i) {
console.log("deleting", i)
$.ajax({
type: "POST",
url: `/api/delete_ex`,
data: {
word_give: word,
number_give: i
},
success: function (response) {
get_examples()
}
});
}
[코드스니펫] - 숙제 시작 코드 파이썬
@app.route('/api/get_exs', methods=['GET'])
def get_exs():
# 예문 가져오기
return jsonify({'result': 'success'})
@app.route('/api/save_ex', methods=['POST'])
def save_ex():
# 예문 저장하기
return jsonify({'result': 'success'})
@app.route('/api/delete_ex', methods=['POST'])
def delete_ex():
# 예문 삭제하기
return jsonify({'result': 'success'})
힌트
delete_many()
를 사용할 수 있어요.2주차 수업을 들으며 기존에 알던 지식에 곱하기 2억이 된거 같습니다!
3주차는 어떨지 벌써 기대되네요!!