웹 사이트로 구현해보자!

자훈·2023년 9월 13일
0

개인프로젝트

목록 보기
2/10
post-thumbnail

📌 코딩이 다가 아니다!

이제껏 우리가 만든 프로그램을 나혼자서만 돌리면 뭐하겠느냐!
op.gg까지는 아니더라도 뭔가 사용하는 느낌이 들어야한다. 그렇기에 오늘 포스팅을 준비해온 것은 로컬사이트(다른 사용자도 접속한 링크를 짤 수 있으나, 아직 보안문제에 대해 해박한 것이 아니기 때문에 로컬로만 진행해보았다.)로 내가 만든 프로그램을 실제와 유사하게 출력값을 화면에 띄우도록 할 것이다!

그렇게 하기 위해서 필요한 준비물은
FLASK 와 HTML이다. 어떤 방식으로 진행해야할지 몰라 막연하게 챗gpt에게 물어본 뒤, 돌아온 답변의 언어 형식으로 만들었다. 처음 배우는 언어와 모듈이기 때문에, 중간에 느릿느릿 그리고 개념을 이해하는데 조금 시간이 걸렸다. 하지만 그 만큼 새로운 희열을 느끼는 결과물을 만들었으니 매우 만족한다! 걸음마를 떼는 단계이겠지만 ㅠㅠ...

📌 FLASK에 스며들기

FlaskPython 웹 애플리케이션을 개발하기 위한 경량 마이크로 웹 프레임워크이다. Flask는 간단하면서도 유연하며 확장 가능한 웹 애플리케이션을 만들 수 있는 도구로서 널리 사용된다.

1. Flask(name):

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 경로가 달라지고, 해당 경로로 가서 아래 함수를 실행한다는 말이다. 다양한 데이터나 리소스를 처리하는데 유용해진다.

2. HTTP 메서드:

FlaskHTTP 요청 메서드(GET, POST, PUT, DELETE 등)에 따라 다른 동작을 수행할 수 있습니다. 예를 들어 @app.route('/login', methods=['GET', 'POST'])와 같이 사용할 수 있습니다.

GET: 정보 요청시 사용. 브라우저에서 링크를 클릭하거나, 주소 표시줄에 URL을 입력하여 사용된다.
POST: 서버에 데이터를 제출하고 데이터를 처리하도록 요청한다. 주로 웹 어플리케이션에서 폼 데이터를 서버로 보내거나 데이터를 업로드할 때 사용한다.

여기서는 이 두가지를 우선 사용한다.

3. 라우트 함수:

라우트 함수는 특정 URL 경로에 대한 처리기 함수를 정의한다. 이 함수는 클라이언트 요청을 처리하고 응답을 반환한다. 라우트 함수를 사용하면 URL과 웹 애플리케이션의 동작을 연결하고, 다양한 동작을 정의할 수 있다.

쉽게 말해 해당 라우트 함수 설정 경로로 요청이 들어오게 될 경우에 함수를 작동하도록 하는 것으로써, 함수끼리의 충돌이 일어나는 것을 라우트 함수를 통해 경로를 설정하여 자연스러운 교통정리가 가능하다는 것이다.

4. 템플릿 엔진:

FlaskJinja2와 함께 사용되며, 템플릿 엔진을 통해 동적 HTML 페이지를 생성할 수 있는데, render_template 함수를 사용하여 HTML 템플릿을 렌더링하고 데이터를 전달한다.

5. 요청 처리:

request 객체는 현재 HTTP 요청에 대한 정보를 제공한다. 예를 들어 request.formPOST 요청의 데이터에 접근하는 데 사용.

6. 응답 생성:

Flask 애플리케이션은 HTTP 응답을 생성하고 반환하는 데 사용되는 여러 함수를 제공한다. return 문을 사용하여 응답을 반환하거나, make_response 함수를 사용하여 응답 객체를 직접 생성 가능하다.

7. 리디렉션:

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 템플릿의 labelaction에서 사용자 입력으로 URL을 입의조작하게 될 때, 경로를 redirection해서 입력 경로로 움직이는 방식으로 작동하는 것에 대한 궁금증을 풀어주는 코드이다.

8. 쿠키와 세션:

request.cookiesresponse.set_cookie를 사용하여 쿠키를 처리하고, 세션 관리를 위해 Flask-Session 등의 확장을 사용할 수 있다.

9. 플라스크 확장:

Flask는 확장을 통해 다양한 추가 기능을 지원한다. 예를 들어 Flask-SQLAlchemy, Flask-WTF, Flask-RESTful 등을 사용하여 데이터베이스 연동, 웹 양식 처리, RESTful API 개발 등을 간단하게 구현할 수 있다.

10. 에러 처리:

@app.errorhandler 데코레이터를 사용하여 특정 HTTP 오류 상황에 대한 처리기 함수를 정의할 수 있다.

11. url_for:

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 할 때, 같이 넘겨주기 때문에 문제없이 템플릿에서 포스팅이 가능하다.

📌 HTML에 스며들기

편의를 위해 wsschool의 클라우드 개발환경을 사용하였습니다.

1. head: 문서의 메타 정보와 외부 리소스를 정의한다. 보통 관념적으로

<meta charset="UTF-8">
<link rel="stylesheet" href="styles.css">

meta charset="UTF-8"을 쓰는데 이는 모든 왠만한 언어드를 사용하겠다는 선언이다. rel 같은 경우에는 <>link문 내에서

link rel = 문서나 리소스 관계 정의 / href = 문서 혹은 URL / type
쉽게 말해 관계선언 / 관련 정보소스 / 정보파일 유형

그리고 head는 추가적으로 문서의 정보에 대해 설명하기도 한다.

2. h1 - h6: 헤딩의 정보로 제목을 나타내는 말. 글자 크기, 중요도로도 생각가능하다. h1으로 갈수록 가장 중요하다.

 <h1>소환사 정보 검색</h1>
    <h2>소환사 정보</h2>

위 템플릿을 플라스크에 실행해보면


이렇게 나오는 것을 확인할 수 있다!
굉장히 신기하다는 것.

3. form: 사용자로부터 데이터를 입력받거나 제출할 때 사용된다.

  • 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>

4. p: 문단 요소로 텍스트를 단락으로 구분한다.

5. table: 표 요소로 데이터를 행과 열의 형태로 구조화하여 표현한다.

6. thead, tbody: 테이블 요소 내의 머리말과 본문을 나타내는 말이다. 표의 구조를 명확하게 하고 CSS, js조작에 유용

tr, th, td: table row, table head, table data의 줄임말로, tr은 각 행을 정의 th로 열의 이름을 정의 tr, td로 행별 열의 데이터를 입력

7. div: 구역을 나타내는 블록요소로 레이아웃을 구성하거나 스타일을 적용하는데 사용되어 진다.

<!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는 강제가 아니다. cssjs에서 요소 식별시 사용한다.
사용자 입력을 여러개 받을 수 있고, 순차적으로도 받을 수 있다.

0개의 댓글