이제껏 우리가 만든 프로그램을 나혼자서만 돌리면 뭐하겠느냐!
op.gg까지는 아니더라도 뭔가 사용하는 느낌이 들어야한다. 그렇기에 오늘 포스팅을 준비해온 것은 로컬사이트(다른 사용자도 접속한 링크를 짤 수 있으나, 아직 보안문제에 대해 해박한 것이 아니기 때문에 로컬로만 진행해보았다.)로 내가 만든 프로그램을 실제와 유사하게 출력값을 화면에 띄우도록 할 것이다!
그렇게 하기 위해서 필요한 준비물은
FLASK 와 HTML이다. 어떤 방식으로 진행해야할지 몰라 막연하게 챗gpt에게 물어본 뒤, 돌아온 답변의 언어 형식으로 만들었다. 처음 배우는 언어와 모듈이기 때문에, 중간에 느릿느릿 그리고 개념을 이해하는데 조금 시간이 걸렸다. 하지만 그 만큼 새로운 희열을 느끼는 결과물을 만들었으니 매우 만족한다! 걸음마를 떼는 단계이겠지만 ㅠㅠ...
Flask
는 Python
웹 애플리케이션을 개발하기 위한 경량 마이크로 웹 프레임워크이다. Flask
는 간단하면서도 유연하며 확장 가능한 웹 애플리케이션을 만들 수 있는 도구로서 널리 사용된다.
Flask
애플리케이션을 생성하는 데 사용되는 클래스이다. name은 현재 모듈의 이름을 나타낸다.
@app.route('/'): 특정 url에 의미있는 단어를 붙여 특정 함수를 연결한다. 해당문의 경우에는 url 주소가 '/'로 끝나는 곳에 함수를 호출한다는 말이다. url 주소에 변수를 지정하게 되면 중괄호를 사용하면 된다.
@app.route('/user/<username>')
def show_user_profile(username):
return f'User {username}'
@app.route('/<username>')
def show_user_profile(username):
return f'User {username}'
바로 하는 것도 가능하다
무슨 말인지 하나도 모를 수 있지만, 쉽게 말해 username 같은 경우에는 사용자 이름을 입력받는데, 입력값에 따라 url 경로가 달라지고, 해당 경로로 가서 아래 함수를 실행한다는 말이다. 다양한 데이터나 리소스를 처리하는데 유용해진다.
Flask
는 HTTP
요청 메서드(GET, POST, PUT, DELETE 등)에 따라 다른 동작을 수행할 수 있습니다. 예를 들어 @app.route('/login', methods=['GET', 'POST'])와 같이 사용할 수 있습니다.
GET: 정보 요청시 사용. 브라우저에서 링크를 클릭하거나, 주소 표시줄에 URL을 입력하여 사용된다.
POST: 서버에 데이터를 제출하고 데이터를 처리하도록 요청한다. 주로 웹 어플리케이션에서 폼 데이터를 서버로 보내거나 데이터를 업로드할 때 사용한다.
여기서는 이 두가지를 우선 사용한다.
라우트 함수는 특정 URL 경로에 대한 처리기 함수를 정의한다. 이 함수는 클라이언트 요청을 처리하고 응답을 반환한다. 라우트 함수를 사용하면 URL과 웹 애플리케이션의 동작을 연결하고, 다양한 동작을 정의할 수 있다.
쉽게 말해 해당 라우트 함수 설정 경로로 요청이 들어오게 될 경우에 함수를 작동하도록 하는 것으로써, 함수끼리의 충돌이 일어나는 것을 라우트 함수를 통해 경로를 설정하여 자연스러운 교통정리가 가능하다는 것이다.
Flask
는 Jinja2
와 함께 사용되며, 템플릿 엔진을 통해 동적 HTML 페이지를 생성할 수 있는데, render_template
함수를 사용하여 HTML 템플릿을 렌더링하고 데이터를 전달한다.
request
객체는 현재 HTTP 요청에 대한 정보를 제공한다. 예를 들어 request.form
은 POST
요청의 데이터에 접근하는 데 사용.
Flask
애플리케이션은 HTTP 응답을 생성하고 반환하는 데 사용되는 여러 함수를 제공한다. return 문을 사용하여 응답을 반환하거나, make_response
함수를 사용하여 응답 객체를 직접 생성 가능하다.
redirect 함수를 사용하여 다른 URL로 리디렉션할 수 있다.
from flask import Flask, redirect, url_for
@app.route('/submit', methods=['POST'])
def submit():
# 데이터 처리 작업 수행
# ...
# 새로운 URL로 리디렉션
user_input = request.form['username']
return redirect(url_for('show_user', username=user_input))
@app.route('/<username>')
def show_user(username):
return f'Hello, {username}!'
해당 코드는 밑에서 설명할 HTML 템플릿의 label
의 action
에서 사용자 입력으로 URL을 입의조작하게 될 때, 경로를 redirection
해서 입력 경로로 움직이는 방식으로 작동하는 것에 대한 궁금증을 풀어주는 코드이다.
request.cookies
및 response.set_cookie
를 사용하여 쿠키를 처리하고, 세션 관리를 위해 Flask-Session
등의 확장을 사용할 수 있다.
Flask
는 확장을 통해 다양한 추가 기능을 지원한다. 예를 들어 Flask-SQLAlchemy, Flask-WTF, Flask-RESTful
등을 사용하여 데이터베이스 연동, 웹 양식 처리, RESTful API 개발 등을 간단하게 구현할 수 있다.
@app.errorhandler
데코레이터를 사용하여 특정 HTTP 오류 상황에 대한 처리기 함수를 정의할 수 있다.
url_for (url에 연결된 함수명, 전달할 인자값)
redirect(url_for( 함수명))
redirect
와 연결해서 사용한다. 이렇게 하면 라우트로 지정해놓은 링크로 이동하면서 해당 링크에 있는 함수를 실행시킬 수 있다.
blueprint
를 사용하고 있다면, url_for의 형식은
redirect(url_for('blueprint_name.def_name)) 형식이다.
from flask import *
app = Flask(__name__)
@route('/users')
def users():
return 'hello users'
def index():
return redirect(url_for('users')
이렇게 하면 로컬링크/users로 이동하여 hello_users가 나옵니다. 만약 변수를 url_for
과 함께 넘기면서 변수로 링크를 지정하고 싶다면,
url_for(함수명, 함수변수 = '전달변수')
형식으로 만들어주면 된다.
from flask import *
import requests
import sqlite3
# 정상적으로 작동함. index 문법 잘못적으면 오류남
# 현재 매치데이터 소환사 정보 출력됨.
# 이제 db관리 혹은 기능추가로 넘어가면 될듯
app = Flask(__name__)
db = sqlite3.connect("lol.db")
cursor = db.cursor()
# table_name = summoner_name.replace(" ", "_")
cursor.execute('''
CREATE TABLE IF NOT EXISTS summoners (
id TEXT PRIMARY KEY,
puuid TEXT,
name TEXT,
profile_icon_id INTEGER,
summoner_level INTEGER,
queue_type TEXT,
tier TEXT,
rank TEXT,
league_points INTEGER,
wins INTEGER,
losses INTEGER
)
''')
db.commit()
db.close()
"""
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS test(
match_id text,
win text,
kills int,
deaths int,
assists int,
kda real
)
''')
db.commit()
"""
초반부분인데 아직 데이터베이스와 연동하는 기능을 구현하지는 않았다. 정확히말하면 데이터가 저장되고 있는지에 대해 초점을 맞추고 만든 코드가 아니라 웹페이지에서 검색을 통해 데이터를 받아 출력해내는 과정까지에 포커스가 있어 미완성인 부분이다.
api_key = 'Use_your_key'
#html템플릿을 작동시키는 함수
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
summoner_name = request.form['summoner_name']
summoner_info = get_summoner_info(summoner_name)
summoner_puuid = summoner_info['puuid']
if summoner_info:
summoner_id = save_summoner_info(summoner_info)
if summoner_id:
save_summoner_rank(summoner_id, summoner_info)
match_data = match_review(summoner_puuid)
return render_template('index.html', summoner_info=summoner_info, match_data=match_data )
else:
return render_template('index.html', error_message='입력하신 소환사를 찾을 수 없습니다.')
return render_template('index.html', summoner_info=None, match_data=None, error_message=None)
#내장함수
def get_summoner_info(summoner_name):
base_url = 'https://kr.api.riotgames.com/lol/summoner/v4/summoners/by-name/'
url = f'{base_url}{summoner_name}?api_key={api_key}'
response = requests.get(url)
if response.status_code == 200:
summoner_info = response.json()
return summoner_info
else:
return None
def get_summoner_rank(summoner_id):
base_url = (f'https://kr.api.riotgames.com/lol/league/v4/entries/by-summoner/'
f'{summoner_id}?api_key={api_key}')
response = requests.get(base_url)
if response.status_code == 200:
summoner_rank = response.json()
return summoner_rank
else:
return None
def save_summoner_info(summoner_info):
db = sqlite3.connect('lol.db')
cursor = db.cursor()
summoner_id = summoner_info['id']
summoner_puuid = summoner_info['puuid'] # puuid 추출
summoner_name = summoner_info["name"]
summoner_icon = summoner_info["profileIconId"]
summoner_level = summoner_info["summonerLevel"]
cursor.execute('SELECT * FROM summoners WHERE id=?', (summoner_id,))
exsining_data = cursor.fetchone()
if exsining_data:
return summoner_id
else:
cursor.execute('''
INSERT INTO summoners(
id,
puuid,
name,
profile_icon_id,
summoner_level
) VALUES(?, ?, ?, ?, ?)
''', (summoner_id, summoner_puuid, summoner_name, summoner_icon, summoner_level))
db.commit()
db.close()
return summoner_id
def save_summoner_rank(summoner_id , summoner_info):
db = sqlite3.connect('lol.db')
cursor = db.cursor()
summoner_rank = get_summoner_rank(summoner_id)
for entry in summoner_rank:
queue_type = entry["queueType"]
tier = entry["tier"]
rank = entry["rank"]
league_points = entry["leaguePoints"]
wins = entry["wins"]
losses = entry["losses"]
# DB정보 확인
cursor.execute('SELECT * FROM summoners WHERE id=?', (summoner_id,))
existing_data = cursor.fetchone()
if existing_data:
# 이미 존재하는 경우 정보 업데이트
cursor.execute('''
UPDATE summoners
SET queue_type=?, tier=?, rank=?, league_points=?, wins=?, losses=?
WHERE id=?
''', (queue_type, tier, rank, league_points, wins, losses, summoner_id))
db.commit()
db.close()
else:
# 존재하지 않는 경우 정보 저장
cursor.execute('''
INSERT INTO summoners (
queue_type
tier,
rank,
league_points,
wins,
losses
) VALUES (?, ?, ?, ?, ?, ?, ?)
''', (summoner_info["profileIconId"], queue_type, tier, rank, league_points, wins, losses))
print(f'{queue_type} 랭크 정보가 저장되었습니다.')
# 데이터베이스 변경 사항을 저장
db.commit()
db.close()
# match - v5
def match_review(summoner_puuid):
"""
if order_match == 1:
match_type = 420 # 솔로랭크 엔드포인트
else:
match_type = 440 # 자유랭크 엔드포인트
"""
#match_type = request.form('match_type')
#order_games = request.form('order_games')
match_type = 420
order_games = 3
match_url = (f'https://asia.api.riotgames.com/lol/match/v5/matches/by-puuid/{summoner_puuid}/'
f'ids?queue={match_type}&type=ranked&start=0&count={order_games}&api_key={api_key}')
response = requests.get(match_url)
if response.status_code == 200:
summoner_match = response.json()
else:
return None
exctracted_data = []
for m in summoner_match:
base_url = (f"https://asia.api.riotgames.com/lol/match/v5/matches/{m}?api_key={api_key}")
r = requests.get(base_url)
Match_Dto = r.json()
Match_info = Match_Dto['info']
Participants = Match_info['participants'] # List[participantDto] puuid로 정보 접근해야함
for participant in Participants:
if participant['puuid'] == summoner_puuid:
w = participant['win']
k = participant['kills']
d = participant['deaths']
a = participant['assists']
win = 'Win' if w else 'Loss'
try:
kda = (k + a) / d
kda = round(kda, 2)
except ZeroDivisionError:
kda = 999
"""
cursor.execute(f'''
INSERT INTO (
match_id,
win,
kills,
deaths,
assists,
kda
)VALUES(?, ?, ?, ?, ?, ?)
''', (m, win, k, d, a, kda))
"""
exctracted_data.append({'Match_id': m, 'Win': win, 'Kills': k, 'Deaths': d, 'Assists': a, 'Kda': kda})
return exctracted_data
if __name__ == '__main__':
app.run(debug=True)
#host='0.0.0.0'으로 app.run에 넣으면 로컬 컴퓨터를 제외한 곳에서도 접근이 가능한 주소가 만들어진다.
초기의 코드를 보았으면 알겠지만, 데이터 베이스와 연동하여 저장하는 기능이 다양했으나, 웹페이지로 보여주는데 있어 지식적 한계가 있어 이후에 추가적인 기능을 구현해나갈 예정이다. 지금의 핵심은 받은 데이터가 오류없이 전달되어 검색기능을 활성화 하는 것에 중점을 두고 있다.
처음에 매치 데이터를 어떻게 넘기는지에 대한 고민이 많았으나, extracted_data에 딕셔너리 형식으로 저장된 값들을 리스트 1 2 3으로 넘겨 html코드에서는 해당 match_data 만큼 결과를 출력하도록 설계했다.
match_review에서 어려웠던 점은 해당 유저의 데이터를 추출할기 위해서는
MatchDTo 안의 infoDto 안의 participantDto 에서 'puuid'를 통해 해당 유저인지 확인하고, 그 유저의 경기 데이터만 추출해내는 방식으로 작성해야 하기 때문에, 조금 어려움이 있었다.
summoner_match에 저장되는 데이터는 경기고유번호라고 생각하면된다. 템플릿에서 match id의 경우 summoner_match에 저장되어 있는 번호지만, 밑에서 list에 append 할 때, 같이 넘겨주기 때문에 문제없이 템플릿에서 포스팅이 가능하다.
<meta charset="UTF-8">
<link rel="stylesheet" href="styles.css">
meta charset="UTF-8"
을 쓰는데 이는 모든 왠만한 언어드를 사용하겠다는 선언이다. rel 같은 경우에는 <>link문 내에서
link rel = 문서나 리소스 관계 정의 / href = 문서 혹은 URL / type
쉽게 말해 관계선언 / 관련 정보소스 / 정보파일 유형
그리고 head는 추가적으로 문서의 정보에 대해 설명하기도 한다.
<h1>소환사 정보 검색</h1>
<h2>소환사 정보</h2>
위 템플릿을 플라스크에 실행해보면
이렇게 나오는 것을 확인할 수 있다!
굉장히 신기하다는 것.
label
: 입력 필드에 대한 설명.
action
: 데이터가 서버로 보내질 곳을 나타낸다. 제출될 URL지정.
for
: 라벨과 연결된 입력필드의 id와 일치시킨다. 사용자가 라벨을 클릭하면 연결된 입력필드가 활성화된다.
input
: 다양한 형식의 입력필드를 생성. type을 통해 입력필드의 유형. submit
버튼을 생성하려면
input type="submit"
으로 생성
value
: 버튼에 표시되는 텍스트 또는 값을 정의 아래의 코드에서는 제출로 정의
name
: 해당 입력필드의 이름을 지정. 서버로 데이터를 제출할 때 식별자임
<form action="/submit" method="post">
<label for="username">사용자 이름:</label>
<input type="text" id="username" name="username">
<input type="submit" value="제출">
</form>
<!DOCTYPE html>
<html>
<head>
<title>소환사 정보 검색</title>
</head>
<body>
<h1>소환사 정보 검색</h1>
<form method="post">
<label for="summoner_name">소환사 이름:</label>
<input type="text" name="summoner_name" id="summoner_name" value="">
<input type="submit" value="검색">
</form>
<h2>소환사 정보</h2>
<p>소환사 이름: {{ summoner_info.name }}</p>
<p>소환사 레벨: {{ summoner_info.summonerLevel }}</p>
{% if match_data %}
<h2>매치 데이터</h2>
<table>
<thead>
<tr>
<th>Match ID</th>
<th>Win</th>
<th>Kills</th>
<th>Deaths</th>
<th>Assists</th>
<th>KDA</th>
</tr>
</thead>
<tbody>
{% for match in match_data %}
<tr>
<td>{{ match.Match_id }}</td>
<td>{{ match.Win }}</td>
<td>{{ match.Kills }}</td>
<td>{{ match.Deaths }}</td>
<td>{{ match.Assists }}</td>
<td>{{ match.Kda }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<div>
{{ html_table | safe }}
</div>
{% if error_message %}
<p>{{ error_message }}</p>
{% endif %}
</body>
</html>
추가...
템플릿에서 id
는 강제가 아니다. css
나 js
에서 요소 식별시 사용한다.
사용자 입력을 여러개 받을 수 있고, 순차적으로도 받을 수 있다.