# condition_extractor.py """ 로또 조건 추출기 - 1~45에서 랜덤 추출 - 최소/최대 설정 - CSV/엑셀 저장 - 당첨번호 인수 계산 """ import sys import os import re import json import time import random import threading from urllib import request import tkinter as tk from tkinter import ttk, messagebox, filedialog, scrolledtext try: import pandas as pd import polars as pl import numpy as np import xlsxwriter HAS_LIBS = True except ImportError: HAS_LIBS = False VERSION = "1.0" CACHE_FILE = "winning_numbers.json" BUYER_NAME = "홍길동" ICON_FILE = "lotto.ico" # ========== 작업표시줄 아이콘 (Windows 전용) ========== def _set_taskbar_icon(ico_path): try: import ctypes app_id = "lottosesang.condition_extractor" ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) except Exception: pass # ========== 당첨번호 관련 ========== def _fetch_from_web(): url = "https://www.dhlottery.co.kr/lt645/selectPstLt645Info.do?srchLtEpsd=all&_=" + \ str(int(time.time() * 1000)) req = request.Request(url) req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36') req.add_header('Accept', 'application/json') req.add_header('Referer', 'https://www.dhlottery.co.kr/') try: with request.urlopen(req, timeout=30) as response: txt = response.read().decode('utf-8') except Exception as e: raise ConnectionError(f"당첨번호 조회 실패: {str(e)}") episodes = re.findall(r'"ltEpsd"\s*:\s*(\d+)', txt) tm1 = re.findall(r'"tm1WnNo"\s*:\s*(\d+)', txt) tm2 = re.findall(r'"tm2WnNo"\s*:\s*(\d+)', txt) tm3 = re.findall(r'"tm3WnNo"\s*:\s*(\d+)', txt) tm4 = re.findall(r'"tm4WnNo"\s*:\s*(\d+)', txt) tm5 = re.findall(r'"tm5WnNo"\s*:\s*(\d+)', txt) tm6 = re.findall(r'"tm6WnNo"\s*:\s*(\d+)', txt) bonus= re.findall(r'"bnsWnNo"\s*:\s*(\d+)', txt) if not episodes: raise ValueError("당첨번호 데이터를 찾을 수 없습니다.") results = [] for i in range(len(episodes)): try: results.append({ '회차': int(episodes[i]), '번호': sorted([int(tm1[i]), int(tm2[i]), int(tm3[i]), int(tm4[i]), int(tm5[i]), int(tm6[i])]), '보너스': int(bonus[i]) }) except (IndexError, ValueError): continue if not results: raise ValueError("당첨번호 파싱 실패") results.sort(key=lambda x: x['회차'], reverse=True) return results def save_to_cache(data): try: with open(CACHE_FILE, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) except Exception as e: print(f"캐시 저장 실패: {e}") def load_from_cache(): if not os.path.exists(CACHE_FILE): return None try: with open(CACHE_FILE, 'r', encoding='utf-8') as f: data = json.load(f) return data if data else None except: return None def get_cache_info(): if not os.path.exists(CACHE_FILE): return None try: mod_time = os.path.getmtime(CACHE_FILE) mod_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(mod_time)) with open(CACHE_FILE, 'r', encoding='utf-8') as f: data = json.load(f) if data: return { '회차수' : len(data), '최신회차': data[0]['회차'], '저장시간': mod_str } return None except: return None # ========== 조건 추출기 GUI ========== class ConditionExtractorApp: def __init__(self, root): self.root = root self.root.title(f"로또 조건 추출기 v{VERSION}") self.root.geometry("1200x900") self.root.minsize(600, 500) # ★ 최소 크기 줄임 (모바일 대응) self.root.configure(bg='#f5f5f5') self._apply_icon() self.style = ttk.Style() self.style.theme_use('clam') # 반응형 관련 변수 self._last_width = 0 self._mobile_mode = False # 좌우→상하 전환 기준 self.winning_data = None self.extracted_conditions = [] self.setup_ui() self.load_cached_data() # ★ 창 크기 변경 이벤트 바인딩 self.root.bind('', self._on_resize) # ── 아이콘 적용 ───────────────────────────────────────────────── def _apply_icon(self): try: base_path = sys._MEIPASS except Exception: base_path = os.path.dirname(os.path.abspath(__file__)) ico_path = os.path.join(base_path, ICON_FILE) if not os.path.exists(ico_path): return try: _set_taskbar_icon(ico_path) self.root.iconbitmap(default=ico_path) except Exception as e: print(f"아이콘 적용 실패: {e}") # ========== 반응형: 창 크기 변경 감지 ========== def _on_resize(self, event): # root 창 자체의 이벤트만 처리 if event.widget != self.root: return w = event.width if w == self._last_width: return self._last_width = w # ── 제목/저작권 폰트 동적 조정 ────────────────────────────── # 창 너비에 비례해 폰트 크기 계산 title_size = max(12, min(28, w // 42)) sub_size = max(7, min(11, w // 110)) buyer_size = max(7, min(11, w // 110)) footer_size = max(7, min(10, w // 130)) self.header_title_lbl.config(font=('맑은 고딕', title_size, 'bold')) self.header_sub_lbl.config(font=('맑은 고딕', sub_size)) self.header_buyer_lbl.config(font=('맑은 고딕', buyer_size, 'bold')) self.footer_lbl.config(font=('맑은 고딕', footer_size)) # ── 좌우 ↔ 상하 레이아웃 전환 (800px 기준) ────────────────── is_mobile = (w < 800) if is_mobile == self._mobile_mode: return self._mobile_mode = is_mobile if is_mobile: # 상하 배치로 전환 self.left_frame.pack_forget() self.right_frame.pack_forget() self.left_frame.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=(0, 5)) self.right_frame.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=(5, 0)) else: # 좌우 배치로 복원 self.left_frame.pack_forget() self.right_frame.pack_forget() self.left_frame.pack( side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5)) self.right_frame.pack( side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0)) # ========== UI 구성 ========== def setup_ui(self): # ── 헤더 ──────────────────────────────────────────────────── self.header = tk.Frame(self.root, bg='#1e3a8a', height=80) self.header.pack(fill=tk.X, side=tk.TOP) self.header.pack_propagate(False) # 제목 (★ 위젯을 인스턴스 변수로 저장 → 반응형 폰트 조정용) self.header_title_lbl = tk.Label( self.header, text="🎰 로또 조건 추출기", font=('맑은 고딕', 28, 'bold'), fg='white', bg='#1e3a8a') self.header_title_lbl.place(relx=0.5, rely=0.28, anchor='center') # 부제 self.header_sub_lbl = tk.Label( self.header, text="랜덤 조건 생성 → 저장 → 당첨번호 인수 계산", font=('맑은 고딕', 10), fg='#93c5fd', bg='#1e3a8a') self.header_sub_lbl.place(relx=0.5, rely=0.70, anchor='center') # 구매자 (오른쪽 끝) self.header_buyer_lbl = tk.Label( self.header, text=f"구매자 : {BUYER_NAME}", font=('맑은 고딕', 11, 'bold'), fg='white', bg='#1e3a8a') self.header_buyer_lbl.place(relx=0.995, rely=0.5, anchor='e') # ── 푸터 (★ 메인보다 먼저 pack) ───────────────────────────── self.footer = tk.Frame(self.root, bg='#1e3a8a') self.footer.pack(fill=tk.X, side=tk.BOTTOM) self.footer_lbl = tk.Label( self.footer, text="ⓒ 2026 로또세상. All rights reserved.", font=('맑은 고딕', 9), fg='#93c5fd', bg='#1e3a8a', pady=5) self.footer_lbl.pack() # ── 메인 프레임 ───────────────────────────────────────────── self.main_frame = tk.Frame(self.root, bg='#f5f5f5', padx=10, pady=10) self.main_frame.pack(fill=tk.BOTH, expand=True) # ★ 좌/우 프레임을 인스턴스 변수로 저장 → 반응형 레이아웃 전환용 self.left_frame = tk.Frame(self.main_frame, bg='#f5f5f5') self.right_frame = tk.Frame(self.main_frame, bg='#f5f5f5') self.left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5)) self.right_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0)) self._build_left(self.left_frame) self._build_right(self.right_frame) # ========== 왼쪽 패널 ========== def _build_left(self, parent): # ── 1. 조건 설정 ── f1 = tk.LabelFrame(parent, text=" 1. 조건 설정 ", font=('맑은 고딕', 11, 'bold'), fg='#1e40af', bg='#ffffff', bd=2, relief=tk.GROOVE) f1.pack(fill=tk.X, pady=(0, 8), ipady=6) i1 = tk.Frame(f1, bg='#ffffff') i1.pack(fill=tk.X, padx=10, pady=6) row = 0 # 조건 최소 / 최대 tk.Label(i1, text="조건 최소(0~6):", font=('맑은 고딕', 9), bg='#ffffff').grid( row=row, column=0, sticky='e', padx=(0, 4), pady=3) self.cond_min_entry = tk.Entry(i1, width=5, font=('맑은 고딕', 10), relief=tk.SUNKEN, bd=2) self.cond_min_entry.grid(row=row, column=1, sticky='w', pady=3) self.cond_min_entry.insert(0, "1") tk.Label(i1, text="조건 최대(0~6):", font=('맑은 고딕', 9), bg='#ffffff').grid( row=row, column=2, sticky='e', padx=(8, 4), pady=3) self.cond_max_entry = tk.Entry(i1, width=5, font=('맑은 고딕', 10), relief=tk.SUNKEN, bd=2) self.cond_max_entry.grid(row=row, column=3, sticky='w', pady=3) self.cond_max_entry.insert(0, "2") row += 1 # 대상수 및 생성개수 tk.Label(i1, text="대상수:생성수:", font=('맑은 고딕', 9, 'bold'), bg='#ffffff').grid( row=row, column=0, sticky='e', padx=(0, 4), pady=3) self.target_config_entry = tk.Entry(i1, width=24, font=('맑은 고딕', 10), relief=tk.SUNKEN, bd=2) self.target_config_entry.grid(row=row, column=1, columnspan=3, sticky='ew', pady=3) self.target_config_entry.insert(0, "20:100, 25:150") self.target_config_entry.bind('', self._update_total_count) row += 1 # 총 생성 예정 self.total_count_label = tk.Label(i1, text="총 생성 예정: 250개 (최대 500개)", font=('맑은 고딕', 9, 'bold'), bg='#ffffff', fg='#2563eb') self.total_count_label.grid( row=row, column=0, columnspan=4, sticky='w', padx=3, pady=(2, 4)) row += 1 tk.Label(i1, text="💡 예: 20:100, 25:150 → 20개짜리 100개 + 25개짜리 150개", font=('맑은 고딕', 8), bg='#ffffff', fg='#6b7280').grid( row=row, column=0, columnspan=4, sticky='w', pady=(0, 2)) # ── 2. 추출 실행 ── f2 = tk.LabelFrame(parent, text=" 2. 추출 실행 ", font=('맑은 고딕', 11, 'bold'), fg='#047857', bg='#ffffff', bd=2, relief=tk.GROOVE) f2.pack(fill=tk.X, pady=(0, 8), ipady=4) i2 = tk.Frame(f2, bg='#ffffff') i2.pack(fill=tk.X, padx=10, pady=6) self.extract_btn = tk.Button(i2, text="🎲 조건 랜덤 추출", command=self.extract_conditions, font=('맑은 고딕', 12, 'bold'), bg='#059669', fg='white', activebackground='#047857', relief=tk.RAISED, bd=3, height=2, cursor='hand2') self.extract_btn.pack(fill=tk.X, pady=4) # ★ fill=X 로 너비 자동 self.extract_btn.bind('', lambda e: self.extract_btn.config(bg='#047857')) self.extract_btn.bind('', lambda e: self.extract_btn.config(bg='#059669')) self.extract_info = tk.Label(i2, text="추출 버튼을 눌러 조건을 생성하세요.", font=('맑은 고딕', 9), bg='#ffffff', fg='#6b7280') self.extract_info.pack(pady=(0, 3)) # ── 3. 파일 저장 ── f3 = tk.LabelFrame(parent, text=" 3. 파일 저장 ", font=('맑은 고딕', 11, 'bold'), fg='#b45309', bg='#ffffff', bd=2, relief=tk.GROOVE) f3.pack(fill=tk.X, pady=(0, 8), ipady=4) i3 = tk.Frame(f3, bg='#ffffff') i3.pack(fill=tk.X, padx=10, pady=6) bf = tk.Frame(i3, bg='#ffffff') bf.pack(fill=tk.X) tk.Button(bf, text="💾 CSV 저장", command=lambda: self.save_file('csv'), font=('맑은 고딕', 10, 'bold'), bg='#10b981', fg='white', relief=tk.RAISED, bd=3, cursor='hand2').pack( side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 4)) tk.Button(bf, text="📊 엑셀 저장", command=lambda: self.save_file('xlsx'), font=('맑은 고딕', 10, 'bold'), bg='#3b82f6', fg='white', relief=tk.RAISED, bd=3, cursor='hand2').pack( side=tk.LEFT, fill=tk.X, expand=True) tk.Label(i3, text='💡 파일명: "추출된 조건.csv" 또는 "추출된 조건.xlsx"', font=('맑은 고딕', 8), bg='#ffffff', fg='#6b7280').pack( anchor=tk.W, pady=(5, 0)) # ── 4. 추출된 조건 목록 ── f4 = tk.LabelFrame(parent, text=" 4. 추출된 조건 목록 ", font=('맑은 고딕', 11, 'bold'), fg='#6b21a8', bg='#ffffff', bd=2, relief=tk.GROOVE) f4.pack(fill=tk.BOTH, expand=True, ipady=4) i4 = tk.Frame(f4, bg='#ffffff') i4.pack(fill=tk.BOTH, expand=True, padx=8, pady=6) self.cond_count_label = tk.Label(i4, text="총 0개 조건", font=('맑은 고딕', 9, 'bold'), bg='#ffffff', fg='#6b7280') self.cond_count_label.pack(anchor=tk.W, pady=(0, 3)) lc = tk.Frame(i4, bg='#ffffff') lc.pack(fill=tk.BOTH, expand=True) lvs = ttk.Scrollbar(lc, orient=tk.VERTICAL) lvs.pack(side=tk.RIGHT, fill=tk.Y) lhs = ttk.Scrollbar(lc, orient=tk.HORIZONTAL) lhs.pack(side=tk.BOTTOM, fill=tk.X) self.cond_text = tk.Text(lc, font=('Consolas', 8), wrap='none', yscrollcommand=lvs.set, xscrollcommand=lhs.set, state='disabled', relief=tk.SUNKEN, bd=2) self.cond_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) lvs.config(command=self.cond_text.yview) lhs.config(command=self.cond_text.xview) # 실시간 총 생성 개수 표시 def _update_total_count(self, event=None): val = self.target_config_entry.get().strip() if not val: self.total_count_label.config( text="총 생성 예정: 0개 (최대 500개)", fg='#6b7280') return total = 0 try: for item in val.split(','): item = item.strip() if not item: continue _, c_str = item.split(':') total += int(c_str.strip()) if total > 500: self.total_count_label.config( text=f"총 생성 예정: {total}개 (❌ 500개 초과)", fg='#dc2626') else: self.total_count_label.config( text=f"총 생성 예정: {total}개 (최대 500개)", fg='#2563eb') except: self.total_count_label.config( text="입력 형식 오류 (예: 20:100, 25:150)", fg='#dc2626') # ========== 오른쪽 패널 ========== def _build_right(self, parent): # ── 5. 당첨번호 불러오기 ── f5 = tk.LabelFrame(parent, text=" 5. 당첨번호 불러오기 ", font=('맑은 고딕', 11, 'bold'), fg='#1e40af', bg='#ffffff', bd=2, relief=tk.GROOVE) f5.pack(fill=tk.X, pady=(0, 8), ipady=4) i5 = tk.Frame(f5, bg='#ffffff') i5.pack(fill=tk.X, padx=10, pady=6) tk.Button(i5, text="📡 최신 당첨번호 불러오기 (인터넷)", command=self.fetch_winning_online, font=('맑은 고딕', 9, 'bold'), bg='#3b82f6', fg='white', relief=tk.RAISED, bd=3, cursor='hand2', pady=4).pack(fill=tk.X) # ★ fill=X self.fetch_label = tk.Label(i5, text="", font=('맑은 고딕', 8), bg='#ffffff', fg='#6b7280') self.fetch_label.pack(anchor=tk.W, pady=(4, 0)) self.cache_label = tk.Label(i5, text="", font=('맑은 고딕', 8), bg='#ffffff', fg='#059669') self.cache_label.pack(anchor=tk.W, pady=(2, 0)) sel_f = tk.Frame(i5, bg='#ffffff') sel_f.pack(fill=tk.X, pady=(8, 0)) tk.Label(sel_f, text="회차 선택:", font=('맑은 고딕', 9), bg='#ffffff').pack(side=tk.LEFT, padx=(0, 6)) self.round_var = tk.StringVar() self.round_combo = ttk.Combobox(sel_f, textvariable=self.round_var, state='readonly') self.round_combo.pack(side=tk.LEFT, fill=tk.X, expand=True) # ★ fill=X # ── 6. 인수 계산 ── f6 = tk.LabelFrame(parent, text=" 6. 인수 계산 ", font=('맑은 고딕', 11, 'bold'), fg='#047857', bg='#ffffff', bd=2, relief=tk.GROOVE) f6.pack(fill=tk.X, pady=(0, 8), ipady=4) i6 = tk.Frame(f6, bg='#ffffff') i6.pack(fill=tk.X, padx=10, pady=6) self.calc_btn = tk.Button(i6, text="📊 인수 계산 실행", command=self.calculate_score, font=('맑은 고딕', 12, 'bold'), bg='#7c3aed', fg='white', activebackground='#6d28d9', relief=tk.RAISED, bd=3, height=2, cursor='hand2') self.calc_btn.pack(fill=tk.X, pady=4) # ★ fill=X self.calc_btn.bind('', lambda e: self.calc_btn.config(bg='#6d28d9')) self.calc_btn.bind('', lambda e: self.calc_btn.config(bg='#7c3aed')) self.calc_info = tk.Label(i6, text="조건 추출 후 회차를 선택하고 인수 계산하세요.", font=('맑은 고딕', 9), bg='#ffffff', fg='#6b7280') self.calc_info.pack(pady=(0, 3)) # ── 7. 인수 계산 결과 ── f7 = tk.LabelFrame(parent, text=" 7. 인수 계산 결과 ", font=('맑은 고딕', 11, 'bold'), fg='#b45309', bg='#ffffff', bd=2, relief=tk.GROOVE) f7.pack(fill=tk.BOTH, expand=True, ipady=4) i7 = tk.Frame(f7, bg='#ffffff') i7.pack(fill=tk.BOTH, expand=True, padx=8, pady=6) self.result_summary = tk.Label(i7, text="", font=('맑은 고딕', 9, 'bold'), bg='#ffffff', fg='#1e40af', justify=tk.LEFT, wraplength=400) # ★ wraplength: 좁은 화면 줄바꿈 self.result_summary.pack(side=tk.TOP, anchor=tk.W, pady=(0, 5)) # 저장/복사 버튼 (하단 고정) bf7 = tk.Frame(i7, bg='#ffffff') bf7.pack(side=tk.BOTTOM, fill=tk.X, pady=(6, 0)) tk.Button(bf7, text="💾 결과 CSV", command=lambda: self.save_result('csv'), font=('맑은 고딕', 9, 'bold'), bg='#10b981', fg='white', relief=tk.RAISED, bd=3, cursor='hand2').pack( side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 3)) tk.Button(bf7, text="📊 결과 엑셀", command=lambda: self.save_result('xlsx'), font=('맑은 고딕', 9, 'bold'), bg='#3b82f6', fg='white', relief=tk.RAISED, bd=3, cursor='hand2').pack( side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 3)) tk.Button(bf7, text="📋 복사", command=self.copy_result, font=('맑은 고딕', 9, 'bold'), bg='#f59e0b', fg='white', relief=tk.RAISED, bd=3, cursor='hand2').pack( side=tk.LEFT, fill=tk.X, expand=True) # 결과 텍스트 rs = tk.Frame(i7, bg='#ffffff') rs.pack(side=tk.TOP, fill=tk.BOTH, expand=True) rvs = ttk.Scrollbar(rs, orient=tk.VERTICAL) rvs.pack(side=tk.RIGHT, fill=tk.Y) rhs = ttk.Scrollbar(rs, orient=tk.HORIZONTAL) rhs.pack(side=tk.BOTTOM, fill=tk.X) self.result_text = tk.Text(rs, font=('Consolas', 9), wrap='none', yscrollcommand=rvs.set, xscrollcommand=rhs.set, relief=tk.SUNKEN, bd=2) self.result_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) rvs.config(command=self.result_text.yview) rhs.config(command=self.result_text.xview) # ========== 당첨번호 로드 ========== def load_cached_data(self): cache_info = get_cache_info() if cache_info: data = load_from_cache() if data: self.winning_data = data self._update_combo(data) self.cache_label.config( text=f"💾 저장: {cache_info['회차수']}개 " f"(최신: {cache_info['최신회차']}회, " f"{cache_info['저장시간']})") self.fetch_label.config( text=f"✅ {cache_info['회차수']}개 회차 로드됨", fg='#059669') else: self.fetch_label.config(text="⏳ 당첨번호를 불러오세요", fg='#6b7280') self.cache_label.config(text="💾 저장된 데이터 없음") def fetch_winning_online(self): self.fetch_label.config(text="📡 불러오는 중...", fg='#2563eb') self.root.update() def _fetch(): try: data = _fetch_from_web() save_to_cache(data) self.winning_data = data self.root.after(0, lambda: self._update_combo(data)) self.root.after(0, lambda: self.fetch_label.config( text=f"✅ {len(data)}개 회차 완료 (최신: {data[0]['회차']}회)", fg='#059669')) info = get_cache_info() if info: self.root.after(0, lambda: self.cache_label.config( text=f"💾 저장: {info['회차수']}개 (최신: {info['최신회차']}회)")) except Exception as e: self.root.after(0, lambda: self.fetch_label.config( text=f"❌ 오류: {str(e)}", fg='#dc2626')) threading.Thread(target=_fetch, daemon=True).start() def _update_combo(self, data): items = [] for d in data: nums = ', '.join(map(str, d['번호'])) items.append(f"{d['회차']}회 - [{nums}] +보너스 {d['보너스']}") self.round_combo['values'] = items if items: self.round_combo.current(0) # ========== 조건 추출 ========== def extract_conditions(self): try: c_min = int(self.cond_min_entry.get()) c_max = int(self.cond_max_entry.get()) if not (0 <= c_min <= 6 and 0 <= c_max <= 6): raise ValueError if c_min > c_max: raise ValueError except ValueError: messagebox.showerror("오류", "조건 최소/최대는 0~6 범위의 숫자로 입력하세요.") return val = self.target_config_entry.get().strip() configs = [] total_count = 0 try: for item in val.split(','): item = item.strip() if not item: continue t_str, c_str = item.split(':') t_val = int(t_str.strip()) c_val = int(c_str.strip()) if not (1 <= t_val <= 45): messagebox.showerror("오류", f"대상수는 1~45 범위여야 합니다.\n(입력값: {t_val})") return if c_val < 1: messagebox.showerror("오류", f"생성 개수는 1 이상이어야 합니다.\n(입력값: {c_val})") return configs.append((t_val, c_val)) total_count += c_val except Exception: messagebox.showerror("오류", "대상수 및 생성개수 형식이 올바르지 않습니다.\n\n" "[입력 예시]\n20:100, 25:150\n" "(대상수 20개짜리 100개, 25개짜리 150개 추출)") return if total_count < 1 or total_count > 500: messagebox.showerror("오류", f"총 생성 개수의 합은 1~500개 사이여야 합니다.\n(현재 총합: {total_count}개)") return self.extracted_conditions = [] for t_val, c_val in configs: for _ in range(c_val): targets = sorted(random.sample(range(1, 46), t_val)) self.extracted_conditions.append({ 'min' : c_min, 'max' : c_max, 'targets': targets }) random.shuffle(self.extracted_conditions) self._refresh_condition_list() self.extract_info.config( text=f"✅ 총 {total_count}개 조건 추출 완료!", fg='#059669') def _refresh_condition_list(self): count = len(self.extracted_conditions) self.cond_count_label.config( text=f"총 {count}개 조건", fg='#1e40af' if count > 0 else '#6b7280') self.cond_text.config(state='normal') self.cond_text.delete('1.0', tk.END) header = f"{'No':>5} {'최소':>4} {'최대':<2} {'대상수':<12}\n" header += "-" * 120 + "\n" self.cond_text.insert(tk.END, header) for i, c in enumerate(self.extracted_conditions): targets_str = ', '.join(map(str, c['targets'])) line = (f"{i+1:>5} {c['min']:>4} {c['max']:>4} " f"({len(c['targets'])}개) {targets_str}\n") self.cond_text.insert(tk.END, line) self.cond_text.config(state='disabled') # ========== 파일 저장 ========== def save_file(self, fmt): if not self.extracted_conditions: messagebox.showwarning("경고", "먼저 조건을 추출하세요.") return if fmt == 'csv': fn = filedialog.asksaveasfilename( defaultextension=".csv", filetypes=[("CSV", "*.csv")], initialfile="추출된 조건.csv") else: fn = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel", "*.xlsx")], initialfile="추출된 조건.xlsx") if not fn: return try: self._write_file(fn, fmt) messagebox.showinfo("완료", f"저장 완료!\n파일: {os.path.basename(fn)}\n" f"조건: {len(self.extracted_conditions)}개") except Exception as e: messagebox.showerror("오류", f"저장 실패: {str(e)}") def _write_file(self, fn, fmt): conditions = self.extracted_conditions num_conditions = len(conditions) max_targets = max(len(c['targets']) for c in conditions) num_rows = 2 + max_targets data_cols = {} for ci, cond in enumerate(conditions): col_data = [cond['min'], cond['max']] + cond['targets'] while len(col_data) < num_rows: col_data.append(None) data_cols[f'조건{ci+1}'] = col_data if fmt == 'csv': import csv with open(fn, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) for row_i in range(num_rows): row = [] for ci in range(num_conditions): val = data_cols[f'조건{ci+1}'][row_i] row.append(val if val is not None else '') writer.writerow(row) else: import xlsxwriter workbook = xlsxwriter.Workbook(fn) worksheet = workbook.add_worksheet("추출된 조건") for ci, cond in enumerate(conditions): worksheet.write(0, ci, cond['min']) worksheet.write(1, ci, cond['max']) for ti, t in enumerate(cond['targets']): worksheet.write(2 + ti, ci, t) workbook.close() # ========== 인수 계산 ========== def calculate_score(self): if not self.extracted_conditions: messagebox.showwarning("경고", "먼저 조건을 추출하세요.") return if self.winning_data is None: messagebox.showwarning("경고", "먼저 당첨번호를 불러오세요.") return selected_idx = self.round_combo.current() if selected_idx < 0: messagebox.showwarning("경고", "회차를 선택하세요.") return winning_info = self.winning_data[selected_idx] winning_nums = set(winning_info['번호']) total_conds = len(self.extracted_conditions) results = [] passed = 0 failed = 0 for i, cond in enumerate(self.extracted_conditions): targets = set(cond['targets']) match_count = len(winning_nums & targets) if cond['min'] <= match_count <= cond['max']: ok = True; passed += 1 else: ok = False; failed += 1 results.append({ 'no' : i + 1, 'min' : cond['min'], 'max' : cond['max'], 'target_count': len(cond['targets']), 'match_count' : match_count, 'ok' : ok }) score = passed self.result_summary.config( text=(f"🎰 {winning_info['회차']}회 " f"당첨: {winning_info['번호']} 보너스: {winning_info['보너스']}\n" f"총: {total_conds}개 | 통과: {passed}개 | " f"실패: {failed}개 | 📊 인수: {score}"), fg='#1e40af') self.result_text.delete('1.0', tk.END) header = (f"{'No':>5} {'최소':>4} {'최대':<3} " f"{'대상수':<4} {'매칭':<4} {'결과':<12}\n") header += "-" * 60 + "\n" self.result_text.insert(tk.END, header) for r in results: result_str = "✅ 통과" if r['ok'] else "❌ 실패" line = (f"{r['no']:>5} {r['min']:>4} {r['max']:>4} " f"{r['target_count']:>6} {r['match_count']:>4} {result_str}\n") self.result_text.insert(tk.END, line) self.result_text.insert(tk.END, "\n" + "=" * 60 + "\n") self.result_text.insert(tk.END, f"총 {total_conds}개 → 통과 {passed}개 / 실패 {failed}개\n") self.result_text.insert(tk.END, f"📊 인수 = {score}\n") self.result_text.see('1.0') self.calc_info.config( text=f"✅ 완료! 인수: {score} (통과: {passed} / 실패: {failed})", fg='#059669') self._last_results = results self._last_winning = winning_info self._last_score = score # ========== 결과 저장 ========== def save_result(self, fmt): if not hasattr(self, '_last_results'): messagebox.showwarning("경고", "먼저 인수 계산을 실행하세요.") return if fmt == 'csv': fn = filedialog.asksaveasfilename( defaultextension=".csv", filetypes=[("CSV", "*.csv")], initialfile="인수_계산결과.csv") else: fn = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel", "*.xlsx")], initialfile="인수_계산결과.xlsx") if not fn: return try: wi = self._last_winning rows = [] for r in self._last_results: rows.append([ r['no'], r['min'], r['max'], r['target_count'], r['match_count'], '통과' if r['ok'] else '실패' ]) if fmt == 'csv': import csv with open(fn, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) writer.writerow([ f"{wi['회차']}회", f"당첨번호: {wi['번호']}", f"보너스: {wi['보너스']}", f"인수: {self._last_score}" ]) writer.writerow(['No','최소','최대','대상수개수','매칭수','결과']) writer.writerows(rows) else: import xlsxwriter workbook = xlsxwriter.Workbook(fn) ws = workbook.add_worksheet("인수계산결과") title_fmt = workbook.add_format({ 'bold': True, 'bg_color': '#1e3a8a', 'font_color': 'white', 'align': 'center', 'valign': 'vcenter' }) pass_fmt = workbook.add_format({ 'bg_color': '#dcfce7', 'align': 'center', 'valign': 'vcenter' }) fail_fmt = workbook.add_format({ 'bg_color': '#fee2e2', 'align': 'center', 'valign': 'vcenter' }) ws.merge_range(0, 0, 0, 5, f"{wi['회차']}회 당첨번호: {wi['번호']} " f"+보너스: {wi['보너스']} | 인수: {self._last_score}", title_fmt) headers = ['No','최소','최대','대상수개수','매칭수','결과'] for ci, h in enumerate(headers): ws.write(1, ci, h, title_fmt) for ri, row in enumerate(rows): fmt_use = pass_fmt if row[5] == '통과' else fail_fmt for ci, val in enumerate(row): ws.write(2 + ri, ci, val, fmt_use) ws.set_column(0, 5, 12) workbook.close() messagebox.showinfo("완료", f"결과 저장 완료!\n파일: {os.path.basename(fn)}") except Exception as e: messagebox.showerror("오류", f"저장 실패: {str(e)}") def copy_result(self): content = self.result_text.get('1.0', tk.END) self.root.clipboard_clear() self.root.clipboard_append(content) messagebox.showinfo("복사", "결과 복사됨!") # ========== 메인 실행 ========== def run(): root = tk.Tk() root.state('zoomed') app = ConditionExtractorApp(root) root.mainloop() if __name__ == "__main__": run()