프로젝트 개요
- 최종프로젝트로 진행했던 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
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()
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
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()
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)로 만드는 라이브러리
pip install pyinstaller
cd myproject/project2
pyinstaller main.py

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

- 여기서 dist폴더에 듣어가면 파이썬 파일명으로된
.exe
파일이 보입니다.

- 해당 프로그램을 클릭해서 실행

결과물
플레이어 조회

플레이어 무기정보 조회

플레이어 이동정보 조회

플레이어 사격정보 조회

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