[Mini Project] SQL Procedure Tool 만들기(Python tkinter 사용)

DeMar_Beom·2023년 8월 27일
0

Mini Project

목록 보기
1/1
post-thumbnail

SQL Procedure Tool

프로젝트 개요

  • 최종프로젝트로 진행했던 Project2의 DB를 활용하여 유점 이름 검색 등을 통해 DB를 확인할 수 있는 SQL Tool제작
  • Python의 tkinter 라이브러리 활용 인터페이스를 제작하고, pyinstaller 활용 .exe프로그램을 제작

SQL Procedure 개발

Procedure는 총 4개 개발

  • 플레이어 기본정보
  • 플레이어 무기정보
  • 플레이어 이동정보
  • 사격부위

플레이어 기본정보

  • 플레이어 기본정보는 services_match_participants라는 테이블에서 where 조건문으로 유저 이름입력 시 기본 정보를 조회하는 프로시저
DELIMITER //

CREATE procedure GetPlayerInfo(in input_name varchar(255))
BEGIN 
	select * 
	from services_match_participants 
	where player_name = input_name;
END //

DELIMITER ;

CALL GetPlayerInfo("Wirush");

플레이어 무기정보

  • services_players테이블(기본정보)을 드라이빙 테이블로 지정해서 services_weapon_masterys테이블(유저 무기정보)를 드리븐 테이블로 account_id컬럼이 같은 유저의 무기정보를 호출하는 프로시저
DELIMITER //

CREATE procedure GetPlayerWeaponInfo(in input_name varchar(255))
begin
select a.player_name, a.account_id, b.first_weapon_name, b.first_weapon_XPtotal, b.second_weapon_name, b.second_weapon_XPtotal, b.third_weapon_name, b.third_weapon_XPtotal, b.weapon_cluster
from services_players a
inner join services_weapon_masterys b
on a.account_id = b.account_id
where player_name = input_name
limit 1;
END //

DELIMITER ;

CALL GetPlayerWeaponInfo("Wirush");

플레이어 이동저보

  • 플레이어의 총 이동정보를 얻는 프로시저
  • 중첩 서브쿼리를 이용하여 services_match_participants 테이블에 입력유저값과 같은 match_id값을 가져오고 그에 따른 x,y좌표 가져오기
  • 그리고 마지막으로 유저가 이동한 총거리를 union을 이용해서 kiiler_x, killer_y 컬럼에 표시
DELIMITER //

CREATE procedure GetPlayerDistance(in input_name varchar(255))
begin
SELECT match_id, victim_name, killer_x, killer_y
FROM services_kill_logs 
WHERE match_id IN (
    SELECT match_id FROM pdgg.services_match_participants
    WHERE player_name = input_name
)
UNION
SELECT NULL, NULL, MAX(killer_x) - MIN(killer_x) as x_difference, MAX(killer_y) - MIN(killer_y) as y_difference
FROM services_kill_logs;
END //

DELIMITER ;


CALL GetPlayerWeaponInfo("Wirush");

플레이어 사격정보

  • 어느 부위를 어떤 유저가 잘 맞추는지 확인해보기 위한 프로시저
  • 사격부위 ('TorsoShot', 'HeadShot', 'ArmShot', 'HeadShot', 'LegShot', 'PelvisShot')를 입력값으로 하고 damage컬럼을 기준으로 내림차순 정렬
DELIMITER //
CREATE PROCEDURE GetAttackersByDamageReason(IN damage_reason VARCHAR(255))
BEGIN
  SELECT attacker_name, attacker_damageReason, SUM(attacker_damage) AS total_damage
  FROM player_fight
  WHERE attacker_damageReason = damage_reason
  GROUP BY attacker_name, attacker_damageReason
  HAVING total_damage = (
    SELECT MAX(subquery.total_damage)
    FROM (
      SELECT attacker_name, SUM(attacker_damage) AS total_damage
      FROM player_fight
      WHERE attacker_damageReason = damage_reason
      GROUP BY attacker_name, attacker_damageReason
    ) AS subquery
    WHERE subquery.attacker_name = player_fight.attacker_name
  )
  ORDER BY 3 desc;
END //
DELIMITER ;


CALL GetAttackersByDamageReason("TorsoShot");

  • 이제 이렇게 만들어진 프로시저를 Python으로 연결

tkinter 라이브러리

  • tkinter는 GUI에 대한 표준 python 인터페이스로, window창을 생성 가능
  • 우선 pymysql로 프로시저가 있는 DB와 연결 (저는 GCP-SQL에서 dump를 떠서 local에 import한 후 진행했습니다)
import pymysql

# pymysql 설정
conn = pymysql.connect(
    host="127.0.0.1",
    user="root",
    password="????",
    database="????"
)
cur = conn.cursor()
  • 이제 tkinter를 사용하는데 우선 Treeview를 생성하는 함수 선언

tkinter Treeview

  • Treeview는 그리드 표를 출력하는데 사용하는 위젯
  • 컬럼명, 순서, 크기 등의 설정가능
def call_procedure(input_name_entry, tree, procedure_name):
    input_name = input_name_entry.get()
    cur.callproc(procedure_name, (input_name,))
    results = cur.fetchall()

    # 조회 결과를 Treeview에 표시
    tree.delete(*tree.get_children())  
    if len(results) > 0:  # 결과가 있을 경우
        for row in results:
            tree.insert("", "end", values=row)
    else:
        tree.insert("", "end", values=("조회 결과 없음",))  # 결과 없음 메시지 추가
  • input_name_entry.get() : tkinter의 'Entry' 위젯에 입력된 텍스트를 값으로 가져오고, 이 값으로 데이터베이스 procedure로 전달
  • cur.callproc(procedure_name, (input_name,)) : 데이터베이스 프로시저를 호출하고, procedure_name은 프로시저의 이름, (input_name,)은 프로시저에 전달한 매개변수
  • 그리고 if문을 통해 결과값이 있으면 tree.insert()를 사용하여 표시되고, 없으면 "조회결과없음"으로 표시
def open_search_window(procedure_name, window_title, columns, label_text): 
    search_window = Toplevel(root)
    search_window.title(window_title)
    search_window.geometry("1500x900")  
    
    input_name_label = Label(search_window, text=label_text + ":")
    input_name_label.pack()
    input_name_entry = Entry(search_window)
    input_name_entry.pack()
    
    call_search_button = Button(search_window, text="검색", command=lambda: call_procedure(input_name_entry, tree, procedure_name))
    call_search_button.pack()

    tree = ttk.Treeview(search_window, columns=[col[0] for col in columns], show="headings")
    for col, width in columns:
        tree.column(col, width=width)
        tree.heading(col, text=col)

    tree.pack()

root = Tk()
root.title("Project Tool")
root.geometry("600x400")

start_frame = Frame(root)
start_frame.pack()
  • open_search_window : 새로운 검색 창을 열고, 검색 조건을 입력 후 검색 버튼 클릭 시, 해당 DB프로시저를 호출 후 결과를 보여줌
  • Toplevel(root) : root 창 위에 새로운 창 생성
  • call_search_button : 검색 버튼을 생성하고, 클릭 시, call_procedure함수를 호출하는 lambda함수 지정
  • tree : 검색결과를 표시하는 ttk.Treeview위젯을 생성하고, columns에 지정된 열정보와 함께 표시할 열을 생성하고, 각 열에 대한 설정을 수행
welcome_label = Label(start_frame, text="플레이어 정보 검색 도구")
welcome_label.pack()
search_button = Button(start_frame, text="플레이어 정보", command=lambda: open_search_window("GetPlayerInfo", "플레이어 조회", [
        ("NUM", 100),
        ("match_id", 100),
        ("player_name", 100),
        ("account_id", 100),
        ("roster_id", 100),
        ("team_ranking", 100),
        ("dbnos", 100),
        ("assists", 100),
        ("damage_dealt", 100),
        ("headshot_kills", 100),
        ("kills", 100),
        ("longest_kill", 100),
        ("team_kills", 100),
        ("ride_distance", 100),
        ("swim_distance", 100),
        ("walk_distance", 100),
        ("players_table_id", 100)
    ], label_text="플레이어 이름"))
search_button.pack()

weapon_search_button = Button(start_frame, text="플레이어 무기정보", command=lambda: open_search_window("GetPlayerWeaponInfo", "플레이어 무기 정보 조회", [
        ("player_name", 100),
        ("account_id", 100),
        ("first_weapon_name", 100),
        ("first_weapon_XPtotal", 100),
        ("second_weapon_name", 100),
        ("second_weapon_XPtotal", 100),
        ("third_weapon_name", 100),
        ("third_weapon_XPtotal", 100),
        ("weapon_cluster", 100)
    ], label_text="플레이어 이름"))
weapon_search_button.pack()

distance_search_button = Button(start_frame, text="플레이어 이동정보", command=lambda: open_search_window("GetPlayerDistance", "플레이어 이동 정보 조회", [
        ("match_id", 100),
        ("victim_name", 100),
        ("killer_x", 100),
        ("killer_y", 100)
    ], label_text="플레이어 이름"))
distance_search_button.pack()

attacker_damagereason_button = Button(start_frame, text="사격부위", command=lambda: open_search_window("GetAttackersByDamageReason", "플레이어 사격 정보 조회", [
        ("attacker_name", 100),
        ("attacker_damagereason", 150),
        ("total_damage", 100)
    ], label_text="사격부위"))
attacker_damagereason_button.pack()


root.mainloop()
  • 각 프로시저에 대한 버튼을 생성하고, 새로운 검색 창과 검색위젯을 생성
  • Entry위젯에 입력된 값을 가져와 DB프로시저 호출하고 GUI 실행

final.py (전체코드)

from tkinter import *
from tkinter import ttk
import pymysql

# pymysql 설정
conn = pymysql.connect(
    host="127.0.0.1",
    user="root",
    password="????",
    database="????"
)
cur = conn.cursor()

def call_procedure(input_name_entry, tree, procedure_name):
    input_name = input_name_entry.get()
    cur.callproc(procedure_name, (input_name,))
    results = cur.fetchall()

    # 조회 결과를 Treeview에 표시
    tree.delete(*tree.get_children())  # 기존 데이터 삭제
    if len(results) > 0:  # 결과가 있을 경우
        for row in results:
            tree.insert("", "end", values=row)
    else:
        tree.insert("", "end", values=("조회 결과 없음",))  # 결과 없음 메시지 추가

def open_search_window(procedure_name, window_title, columns, label_text): 
    search_window = Toplevel(root)
    search_window.title(window_title)
    search_window.geometry("1500x900")  
    
    input_name_label = Label(search_window, text=label_text + ":")
    input_name_label.pack()
    input_name_entry = Entry(search_window)
    input_name_entry.pack()
    
    call_search_button = Button(search_window, text="검색", command=lambda: call_procedure(input_name_entry, tree, procedure_name))
    call_search_button.pack()

    tree = ttk.Treeview(search_window, columns=[col[0] for col in columns], show="headings")
    for col, width in columns:
        tree.column(col, width=width)
        tree.heading(col, text=col)

    tree.pack()

root = Tk()
root.title("Project Tool")
root.geometry("600x400")

start_frame = Frame(root)
start_frame.pack()

welcome_label = Label(start_frame, text="플레이어 정보 검색 도구")
welcome_label.pack()
search_button = Button(start_frame, text="플레이어 정보", command=lambda: open_search_window("GetPlayerInfo", "플레이어 조회", [
        ("NUM", 100),
        ("match_id", 100),
        ("player_name", 100),
        ("account_id", 100),
        ("roster_id", 100),
        ("team_ranking", 100),
        ("dbnos", 100),
        ("assists", 100),
        ("damage_dealt", 100),
        ("headshot_kills", 100),
        ("kills", 100),
        ("longest_kill", 100),
        ("team_kills", 100),
        ("ride_distance", 100),
        ("swim_distance", 100),
        ("walk_distance", 100),
        ("players_table_id", 100)
    ], label_text="플레이어 이름"))
search_button.pack()

weapon_search_button = Button(start_frame, text="플레이어 무기정보", command=lambda: open_search_window("GetPlayerWeaponInfo", "플레이어 무기 정보 조회", [
        ("player_name", 100),
        ("account_id", 100),
        ("first_weapon_name", 100),
        ("first_weapon_XPtotal", 100),
        ("second_weapon_name", 100),
        ("second_weapon_XPtotal", 100),
        ("third_weapon_name", 100),
        ("third_weapon_XPtotal", 100),
        ("weapon_cluster", 100)
    ], label_text="플레이어 이름"))
weapon_search_button.pack()

distance_search_button = Button(start_frame, text="플레이어 이동정보", command=lambda: open_search_window("GetPlayerDistance", "플레이어 이동 정보 조회", [
        ("match_id", 100),
        ("victim_name", 100),
        ("killer_x", 100),
        ("killer_y", 100)
    ], label_text="플레이어 이름"))
distance_search_button.pack()

attacker_damagereason_button = Button(start_frame, text="사격부위", command=lambda: open_search_window("GetAttackersByDamageReason", "플레이어 사격 정보 조회", [
        ("attacker_name", 100),
        ("attacker_damagereason", 150),
        ("total_damage", 100)
    ], label_text="사격부위"))
attacker_damagereason_button.pack()


root.mainloop()

.exe프로그램 변환

  • 이렇게 만든 .py파일을 pyinstaller를 이용해서 .exe파일로 생성

pyinstaller

  • 파이썬으로 개발한 프로그램을 실행 파일(exe)로 만드는 라이브러리
  • pyinstaller라이브러리 설치
pip install pyinstaller
  • final.py 파일이 있는 폴더로 이동
cd myproject/project2
  • 아래 명령어 실행
pyinstaller main.py

  • 위의 화면이 완료되면 dist폴더와 build폴더가 생깁니다.

  • 여기서 dist폴더에 듣어가면 파이썬 파일명으로된 .exe파일이 보입니다.
  • 해당 프로그램을 클릭해서 실행

결과물

플레이어 조회

플레이어 무기정보 조회

플레이어 이동정보 조회

플레이어 사격정보 조회

후기

  • tkinter이라는 라이브러리를 처음 써봤는데 생각보다 어렵지는 않으나 몇 가지 불편한 점 존재
    - 레이아웃 조절이 위젯의 위치와 크기를 조정이 미세하게 불가
    • 비교적 간단한 GUI로 꾸미는 기능이 부족
  • 프로시저를 입력할 때 유저의 이름을 모를 때는 어떡하지라는 생각하면 이 tool은 쓸모가없다...
  • 실제 회사에서 유저 개인개인 이름을 입력할 일이 얼마나 있을까라는 생각이지만 프로시저를 작성하고, tool을 만들었다는 것에 만족하려고 함...

0개의 댓글