메이플스토리에서 주간·일간 보스를 돌며 결정석을 판매하다 보면
“이번 주에 과연 얼마를 벌었을까?”라는 궁금증이 생깁니다.
파이썬의 GUI 라이브러리 Tkinter로 보스 수익 계산기를 제작했습니다.
본문에서는 전체 코드를 일일이 나열하지 않고,
데이터 구조·위젯 배치·이벤트 흐름 관점에서 코드의 핵심을 단계적으로 설명합니다.
글을 끝까지 읽으면 자신의 루틴에 맞게 코드를 수정하거나 확장하는 방법을 자연스럽게 이해할 수 있습니다.
목차
1. 프로그램 개요 및 전체코드
프로그램은 사용자가 체크박스로 보스를 선택하면
BOSS_REWARD 딕셔너리에 저장된 결정석 가격을 실시간으로 합산해 보여줍니다.
Tkinter 기본 위젯만 사용했기 때문에 별도 외부 라이브러리가 필요 없습니다.
import tkinter as tk
from tkinter import ttk
# ── 결정석 가격데이터 ──
BOSS_REWARD = {
("검은 마법사", "익스트림"): 9_200_000_000,
("카링", "익스트림"): 3_150_000_000,
("감시자 칼로스", "익스트림"): 2_700_000_000,
("선택받은 세렌", "익스트림"): 2_420_000_000,
("발드릭스", "하드"): 2_160_000_000,
("림보", "하드"): 1_930_000_000,
("카링", "하드"): 1_310_000_000,
("감시자 칼로스", "카오스"): 1_120_000_000,
("검은 마법사", "하드"): 1_000_000_000,
("림보", "노멀"): 900_000_000,
("카링", "노멀"): 595_000_000,
("스우", "익스트림"): 549_000_000,
("감시자 칼로스", "노멀"): 510_000_000,
("선택받은 세렌", "하드"): 440_000_000,
("카링", "이지"): 381_000_000,
("감시자 칼로스", "이지"): 345_000_000,
("선택받은 세렌", "노멀"): 295_000_000,
("진 힐라", "하드"): 160_000_000,
("듄켈", "하드"): 142_000_000,
("윌", "하드"): 116_000_000,
("가디언 엔젤 슬라임", "카오스"): 113_000_000,
("진 힐라", "노멀"): 107_000_000,
("더스크", "카오스"): 105_000_000,
("루시드", "하드"): 94_500_000,
("스우", "하드"): 77_400_000,
("데미안", "하드"): 73_500_000,
("듄켈", "노멀"): 62_500_000,
("더스크", "노멀"): 57_900_000,
("윌", "노멀"): 54_100_000,
("루시드", "노멀"): 46_900_000,
("윌", "이지"): 42_500_000,
("루시드", "이지"): 39_200_000,
("가디언 엔젤 슬라임", "노멀"): 33_500_000,
("데미안", "노멀"): 23_000_000,
("스우", "노멀"): 22_000_000,
("파풀라투스", "카오스"): 17_300_000,
("벨룸", "카오스"): 9_280_000,
("매그너스", "하드"): 8_560_000,
("피에르", "카오스"): 8_170_000,
("반반", "카오스"): 8_150_000,
("블러디 퀸", "카오스"): 8_140_000,
("자쿰", "카오스"): 8_080_000,
("시그너스", "노멀"): 7_500_000,
("핑크빈", "카오스"): 6_580_000,
("힐라", "하드"): 5_750_000,
("시그너스", "이지"): 4_550_000,
("파풀라투스", "노멀"): 1_520_000,
("매그너스", "노멀"): 1_480_000,
("아카이럼", "노멀"): 1_430_000,
("반 레온", "하드"): 1_390_000,
("반 레온", "노멀"): 830_000,
("핑크빈", "노멀"): 799_000,
("혼테일", "카오스"): 770_000,
("카웅", "-"): 712_000,
("아카이럼", "이지"): 656_000,
("반 레온", "이지"): 602_000,
("혼테일", "노멀"): 576_000,
("피에르", "노멀"): 551_000,
("반반", "노멀"): 551_000,
("블러디 퀸", "노멀"): 551_000,
("벨룸", "노멀"): 551_000,
("혼테일", "이지"): 502_000
}
class ToolTip:
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tipwindow = None
widget.bind("<Enter>", self.show)
widget.bind("<Leave>", self.hide)
def show(self, event=None):
if self.tipwindow:
return
x, y, _, cy = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 25
y += self.widget.winfo_rooty() + 20
self.tipwindow = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.geometry(f"+{x}+{y}")
label = tk.Label(tw, text=self.text, justify='left',
background="#ffffe0", relief='solid', borderwidth=1,
font=("맑은 고딕", 9))
label.pack(ipadx=1)
def hide(self, event=None):
if self.tipwindow:
self.tipwindow.destroy()
self.tipwindow = None
class BossGUI(tk.Tk):
def __init__(self):
super().__init__()
self.title("보스 수익 계산기")
self.geometry("640x600")
self.resizable(False, False)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", self.refresh_list)
tk.Label(self, text="보스 검색").pack(anchor="w", padx=10, pady=(10, 0))
tk.Entry(self, textvariable=self.search_var).pack(fill="x", padx=10)
self.unit_var = tk.StringVar(value="메소")
ttk.Combobox(self, textvariable=self.unit_var, values=["메소", "억 메소"],
width=10, state="readonly").pack(anchor="e", padx=10, pady=(5, 0))
self.unit_var.trace_add("write", lambda *_: self.refresh_total())
button_frame = tk.Frame(self)
button_frame.pack(fill="x", padx=10, pady=5)
tk.Button(button_frame, text="전체 선택", command=self.select_all).pack(side="left")
tk.Button(button_frame, text="전체 해제", command=self.deselect_all).pack(side="left")
self.sort_var = tk.StringVar(value="가격순 🔽")
sort_cb = ttk.Combobox(button_frame, textvariable=self.sort_var, state="readonly",
values=["가격순 🔽", "보스명 🔤", "난이도 순 🔼"], width=12)
sort_cb.pack(side="right")
self.sort_var.trace_add("write", lambda *_: self.sort_and_refresh())
self.canvas = tk.Canvas(self, borderwidth=0, height=430)
inner = tk.Frame(self.canvas)
vbar = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=vbar.set)
vbar.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True, padx=10, pady=10)
self.canvas.create_window((0, 0), window=inner, anchor="nw")
inner.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
self.canvas.bind_all("<Button-4>", self._on_mousewheel)
self.canvas.bind_all("<Button-5>", self._on_mousewheel)
self.list_frame = inner
self.var_dict = {}
self.total_var = tk.StringVar(value="총 수익: 0 메소")
ttk.Separator(self, orient="horizontal").pack(fill="x", padx=10)
tk.Label(self, textvariable=self.total_var,
font=("맑은 고딕", 12, "bold")).pack(pady=6)
self.all_keys = sorted(BOSS_REWARD.keys(), key=lambda k: -BOSS_REWARD[k])
self.build_checkboxes()
self.refresh_total()
def build_checkboxes(self):
# 체크박스 모두 제거
for widget in self.list_frame.winfo_children():
widget.destroy()
self.var_dict.clear()
for boss, diff in self.all_keys:
key = (boss, diff)
var = tk.BooleanVar()
price = BOSS_REWARD[key]
color = "red" if price >= 100_000_000 else "black"
cb = tk.Checkbutton(self.list_frame,
text=f"{boss} ({diff}) {price:,} 메소",
variable=var, anchor="w", justify="left", fg=color,
command=self.refresh_total)
cb.pack(fill="x", anchor="w")
ToolTip(cb, f"{boss} {diff}\n결정석: {price:,} 메소")
self.var_dict[key] = (var, cb)
def refresh_list(self, *_):
query = self.search_var.get().strip().lower()
for (boss, diff), (var, cb) in self.var_dict.items():
text = f"{boss} {diff}".lower()
if query in text:
cb.pack(fill="x", anchor="w")
else:
cb.pack_forget()
def refresh_total(self):
total = sum(BOSS_REWARD[key] for key, (v, _) in self.var_dict.items() if v.get())
unit = self.unit_var.get()
if unit == "억 메소":
total_str = f"{total / 100_000_000:.2f} 억 메소"
else:
total_str = f"{total:,} 메소"
self.total_var.set(f"총 수익: {total_str}")
def select_all(self):
for var, _ in self.var_dict.values():
var.set(True)
self.refresh_total()
def deselect_all(self):
for var, _ in self.var_dict.values():
var.set(False)
self.refresh_total()
def sort_and_refresh(self):
criterion = self.sort_var.get()
if criterion == "가격순 🔽":
self.all_keys = sorted(BOSS_REWARD.keys(), key=lambda k: -BOSS_REWARD[k])
elif criterion == "보스명 🔤":
self.all_keys = sorted(BOSS_REWARD.keys(), key=lambda k: k[0])
elif criterion == "난이도 순 🔼":
order = {"익스트림": 0, "하드": 1, "카오스": 2, "노멀": 3, "이지": 4, "-": 5}
self.all_keys = sorted(BOSS_REWARD.keys(), key=lambda k: order.get(k[1], 6))
for _, cb in self.var_dict.values():
cb.pack_forget()
self.build_checkboxes()
self.refresh_list()
self.refresh_total()
def _on_mousewheel(self, event):
if event.num == 4:
self.canvas.yview_scroll(-1, "units")
elif event.num == 5:
self.canvas.yview_scroll(1, "units")
else:
self.canvas.yview_scroll(-int(event.delta / 120), "units")
app = BossGUI()
app.mainloop()
2. 결정석 데이터 구조
가격 정보는 아래처럼 튜플 키를 사용해 보스명과 난이도를 한 번에 표현합니다.
이 방식은 동일 보스의 난이도별 중복을 자연스럽게 해결하고, 정렬·검색 시 필터 조건을 단순화합니다.
(
("검은 마법사", "익스트림"): 9_200_000_000,
("카링", "하드"): 1_310_000_000,
...
)
개인적으로 여러 프로젝트에서 딕셔너리를 다룰 때
(primary, secondary) 튜플을 키로 쓰면 유연성이 크게 올라간다는 경험이 있습니다.
필터링·그룹화에 비교 연산자 하나만 써도 되기 때문이죠.
3. GUI 위젯 배치 전략
상단 검색창 ― StringVar.trace_add를 이용해 입력 즉시 필터링
단위 선택 콤보박스 ― Combobox의 readonly 옵션으로 선택만 허용
버튼 영역 ― 전체 선택·해제를 왼쪽, 정렬 콤보박스를 오른쪽에 배치
스크롤 목록 ― Canvas에 내부 Frame을 넣어 마우스 휠로 스크롤
총 수익 라벨 ― 하단 고정, 폰트를 굵게 설정
초기에 Listbox를 고려했지만, 체크 상태를 저장해야 했기 때문에 Checkbutton과 Canvas 조합이 더 적합했습니다.
4. 핵심 로직 단계별 흐름
프로그램 시작 시 self.all_keys를 가격순으로 정렬
build_checkboxes()에서 체크박스 위젯을 생성하고 툴팁 할당
사용자가 검색어 입력 → refresh_list()에서 조건 미일치 항목 pack_forget()
체크 상태가 바뀌면 refresh_total()이 호출되어 합산 결과 업데이트
정렬 콤보박스 변경 시 sort_and_refresh()가 리스트를 재정렬하고 다시 위젯을 생성
5. 주요 함수 워크스루
5‑1. build_checkboxes()
체크박스를 처음 생성하거나 정렬 후 재생성할 때 호출됩니다.
위젯을 다시 만들기 전에 winfo_children()로 기존 위젯을 모두 지워야 메모리 누수를 막을 수 있습니다.
for child in self.list_frame.winfo_children():
child.destroy()
self.var_dict.clear()
그 뒤 반복문에서 가격이 1억 이상이면 글씨를 빨간색으로, 아니면 검정으로 지정합니다.
5‑2. refresh_list()
검색창과 직접 연결되어 있으며, 체크박스를 숨기는 방식으로 필터링합니다. 리스트 자체를 재생성하지 않기 때문에 검색 속도가 매우 빠릅니다.
5‑3. refresh_total()
체크된 항목의 가격을 모두 더한 뒤 드롭다운에서 지정한 단위에 맞춰 문자열을 변환합니다. 개인적으로 억 단위를 선호해 100_000_000으로 나눠 소수점 둘째 자리까지 보여주도록 구현했습니다.
5‑4. sort_and_refresh()
정렬 기준을 바꾸면 딕셔너리를 다시 정렬하고 체크박스를 재생성합니다. 가격순·이름순은 파이썬 내장 sorted()로 충분하지만 난이도순은 문자열 의미가 뒤섞이므로 order 딕셔너리를 따로 두어 가중치를 지정했습니다.
6. 확장 아이디어
CSV 내보내기 주차별 선택 기록을 저장해 성장 추적
웹 버전 Flask·Streamlit으로 포팅해 모바일에서도 접근
자동 가격 크롤링 나무위키 최신 가격을 정기적으로 파싱
7. 마무리
이번 글에서는 파이썬 Tkinter로 구현한 메이플스토리 보스 결정석 수익 계산기 코드 구조를 단계적으로 살펴봤습니다.
검색 필터, 정렬, 단위 변환, 툴팁 등 실용적인 기능을 넣었지만 코드 길이는 300줄이 채 되지 않으며,
전적으로 표준 라이브러리만 활용했습니다.
소스코드를 원하는 대로 변형해 자신의 루틴 관리에 활용해 보세요.
'개발 & 코딩' 카테고리의 다른 글
파이썬 tkinter로 만드는 날짜별 할 일 관리 프로그램 (0) | 2025.07.03 |
---|---|
파이썬 for문과 리스트 기초부터 예제까지 (0) | 2025.06.22 |
파이썬 if문과 while문 기초 및 실습 예제 (0) | 2025.06.21 |
파이썬으로 파일 자동 분류 프로그램 만들기 (1) | 2025.06.20 |
Python OCR 이미지 텍스트 인식 성능 알아보기 (0) | 2025.06.19 |