🚚 refactor: lotto-runner 독립 배포 환경으로 분리
- lotto_auto_buy.py를 루트에서 lotto-runner/로 이동 - 로그 경로를 logs/ 하위로 변경 (볼륨 마운트 대응) - Dockerfile: node:22 → python:3.11-bookworm, Chrome 직접 설치 추가 - docker-compose.yml 신규 생성 (NAS Container Manager용) - CI: requirements.txt 및 스크립트 경로를 lotto-runner/ 기준으로 수정 - Slack 알림 전송 기능 구현 (send_slack 함수) - Chrome 컨테이너 구동 안정화 플래그 추가 - popupLayerAlert JS 강제 숨김으로 클릭 인터셉트 해결
This commit is contained in:
4
.github/workflows/lotto-buy.yml
vendored
4
.github/workflows/lotto-buy.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
python3 --version
|
||||
python3 -m venv .venv
|
||||
.venv/bin/python -m pip install --upgrade pip setuptools wheel
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
.venv/bin/pip install -r lotto-runner/requirements.txt
|
||||
|
||||
- name: Chrome 설치
|
||||
uses: browser-actions/setup-chrome@v1
|
||||
@@ -76,4 +76,4 @@ jobs:
|
||||
env:
|
||||
LOTTO_USER_ID: ${{ secrets.LOTTO_USER_ID }}
|
||||
LOTTO_USER_PW: ${{ secrets.LOTTO_USER_PW }}
|
||||
run: .venv/bin/python3 lotto_auto_buy.py
|
||||
run: .venv/bin/python3 lotto-runner/lotto_auto_buy.py
|
||||
|
||||
39
lotto-runner/Dockerfile
Normal file
39
lotto-runner/Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
||||
FROM python:3.11-bookworm
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
# Chrome 시스템 의존성 설치
|
||||
RUN apt-get update && apt-get install -y \
|
||||
fonts-liberation fonts-noto-cjk \
|
||||
libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 \
|
||||
libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 \
|
||||
libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 \
|
||||
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 \
|
||||
libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
|
||||
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \
|
||||
libxss1 libxtst6 xdg-utils curl wget gnupg ca-certificates unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Google Chrome 설치 (.deb 직접 다운로드 방식 - NAS 방화벽에 안정적)
|
||||
RUN wget -q -O /tmp/google-chrome.deb \
|
||||
https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y /tmp/google-chrome.deb \
|
||||
&& rm /tmp/google-chrome.deb \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Python 가상환경 및 패키지 설치
|
||||
RUN python3 -m venv /opt/venv \
|
||||
&& /opt/venv/bin/python -m pip install --upgrade pip setuptools wheel
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt /tmp/requirements.txt
|
||||
RUN /opt/venv/bin/pip install -r /tmp/requirements.txt
|
||||
|
||||
COPY lotto_auto_buy.py /app/lotto_auto_buy.py
|
||||
RUN mkdir -p /app/logs
|
||||
|
||||
CMD ["python", "lotto_auto_buy.py"]
|
||||
16
lotto-runner/docker-compose.yml
Normal file
16
lotto-runner/docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
lotto-runner:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: lotto-runner:latest
|
||||
shm_size: "256mb"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- LOTTO_USER_ID
|
||||
- LOTTO_USER_PW
|
||||
- SLACK_WEBHOOK_URL
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
restart: "no"
|
||||
727
lotto_auto_buy.py → lotto-runner/lotto_auto_buy.py
Executable file → Normal file
727
lotto_auto_buy.py → lotto-runner/lotto_auto_buy.py
Executable file → Normal file
@@ -1,337 +1,390 @@
|
||||
"""
|
||||
동행복권(dhlottery.co.kr) 로또 6/45 자동 구매 스크립트
|
||||
- 매주 금요일 자동 실행 (cron/스케줄러 연동)
|
||||
- 5게임 자동 구매
|
||||
- Selenium 기반 웹 자동화
|
||||
|
||||
사전 준비:
|
||||
pip install selenium
|
||||
Chrome 브라우저 + ChromeDriver 설치 필요
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
# .env 파일 자동 로드
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
except ImportError:
|
||||
pass
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 설정
|
||||
# ─────────────────────────────────────────────
|
||||
CONFIG = {
|
||||
"USER_ID": os.environ.get("LOTTO_USER_ID", "hyeonggil2"),
|
||||
"USER_PW": os.environ.get("LOTTO_USER_PW", "Rlaehdbs062$"),
|
||||
"BUY_COUNT": 1, # 구매 게임 수 (1~10)
|
||||
"HEADLESS": True, # True: 브라우저 창 숨김, False: 브라우저 창 표시
|
||||
"LOG_FILE": "lotto_log.json",
|
||||
"SLACK_WEBHOOK_URL": os.environ.get("SLACK_WEBHOOK_URL", ""),
|
||||
}
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler("lotto_auto.log", encoding="utf-8"),
|
||||
],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_driver():
|
||||
"""Chrome WebDriver 생성"""
|
||||
options = Options()
|
||||
if CONFIG["HEADLESS"]:
|
||||
options.add_argument("--headless=new")
|
||||
options.add_argument("--no-sandbox")
|
||||
options.add_argument("--disable-dev-shm-usage")
|
||||
options.add_argument("--disable-gpu")
|
||||
options.add_argument("--window-size=1920,1080")
|
||||
# 자동화 탐지 우회
|
||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||
options.add_experimental_option("useAutomationExtension", False)
|
||||
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.execute_cdp_cmd(
|
||||
"Page.addScriptToEvaluateOnNewDocument",
|
||||
{"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"},
|
||||
)
|
||||
driver.implicitly_wait(5)
|
||||
return driver
|
||||
|
||||
|
||||
def login(driver):
|
||||
"""동행복권 사이트 로그인"""
|
||||
logger.info("로그인 시도 중...")
|
||||
driver.get("https://www.dhlottery.co.kr/login")
|
||||
time.sleep(2)
|
||||
|
||||
# 아이디/비밀번호 입력
|
||||
wait = WebDriverWait(driver, 10)
|
||||
id_input = wait.until(EC.element_to_be_clickable((By.ID, "inpUserId")))
|
||||
id_input.click()
|
||||
id_input.send_keys(CONFIG["USER_ID"])
|
||||
|
||||
pw_input = wait.until(EC.element_to_be_clickable((By.ID, "inpUserPswdEncn")))
|
||||
pw_input.click()
|
||||
pw_input.send_keys(CONFIG["USER_PW"])
|
||||
|
||||
# 로그인 버튼 클릭
|
||||
login_btn = wait.until(EC.element_to_be_clickable((By.ID, "btnLogin")))
|
||||
login_btn.click()
|
||||
|
||||
# /login이 URL에서 완전히 사라질 때까지 대기 (최대 15초)
|
||||
try:
|
||||
WebDriverWait(driver, 15).until(lambda d: "/login" not in d.current_url)
|
||||
except:
|
||||
pass
|
||||
|
||||
logger.info(f"로그인 후 URL: {driver.current_url}")
|
||||
|
||||
# 로그인 성공 확인 (URL이 /login에서 벗어나면 성공)
|
||||
if "/login" in driver.current_url:
|
||||
logger.error("로그인 실패! 아이디/비밀번호를 확인하세요.")
|
||||
return False
|
||||
|
||||
logger.info("로그인 성공!")
|
||||
return True
|
||||
|
||||
|
||||
def check_balance(driver):
|
||||
"""예치금 잔액 확인"""
|
||||
driver.get("https://www.dhlottery.co.kr/mypage/home")
|
||||
time.sleep(2)
|
||||
try:
|
||||
balance_elem = driver.find_element(By.CSS_SELECTOR, "#totalAmt")
|
||||
balance_text = balance_elem.text.replace(",", "").replace("원", "").strip()
|
||||
balance = int(balance_text) if balance_text.isdigit() else 0
|
||||
logger.info(f"현재 예치금 잔액: {balance:,}원")
|
||||
return balance
|
||||
except Exception as e:
|
||||
logger.warning(f"잔액 확인 실패: {e}")
|
||||
return -1
|
||||
|
||||
|
||||
def buy_lotto(driver, game_count=5):
|
||||
"""
|
||||
로또 6/45 자동 구매
|
||||
game_count: 구매할 게임 수 (1~10)
|
||||
"""
|
||||
logger.info(f"로또 {game_count}게임 자동 구매 시작...")
|
||||
|
||||
# 로또 구매 페이지 이동
|
||||
driver.get("https://ol.dhlottery.co.kr/olotto/game/game645.do")
|
||||
time.sleep(3)
|
||||
|
||||
# iframe 전환 (로또 구매 페이지는 iframe 내부에 있음)
|
||||
try:
|
||||
# 팝업 닫기 (있을 경우)
|
||||
try:
|
||||
close_btns = driver.find_elements(By.CSS_SELECTOR, ".close, .btn_close")
|
||||
for btn in close_btns:
|
||||
try:
|
||||
btn.click()
|
||||
time.sleep(0.5)
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
wait = WebDriverWait(driver, 10)
|
||||
|
||||
# "자동" 선택
|
||||
auto_btn = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, "num2"))
|
||||
)
|
||||
auto_btn.click()
|
||||
time.sleep(1)
|
||||
|
||||
# 게임 수 설정 (드롭다운에서 선택)
|
||||
from selenium.webdriver.support.ui import Select
|
||||
try:
|
||||
select_elem = driver.find_element(By.ID, "amoundApply")
|
||||
select = Select(select_elem)
|
||||
select.select_by_value(str(game_count))
|
||||
time.sleep(0.5)
|
||||
except Exception:
|
||||
# 드롭다운이 없는 경우 자동번호 버튼을 game_count번 클릭
|
||||
for i in range(game_count):
|
||||
auto_select_btn = driver.find_element(By.ID, "btnSelectNum")
|
||||
auto_select_btn.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
# "자동번호 선택" 버튼 클릭
|
||||
try:
|
||||
select_num_btn = driver.find_element(By.ID, "btnSelectNum")
|
||||
select_num_btn.click()
|
||||
time.sleep(1)
|
||||
except:
|
||||
pass
|
||||
|
||||
# "구매하기" 버튼 클릭
|
||||
buy_btn = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, "btnBuy"))
|
||||
)
|
||||
buy_btn.click()
|
||||
time.sleep(2)
|
||||
|
||||
# 구매 확인 팝업 "확인" 클릭
|
||||
confirm_btn = wait.until(
|
||||
EC.element_to_be_clickable((By.CSS_SELECTOR, "#popupLayerConfirm input[value='확인']"))
|
||||
)
|
||||
confirm_btn.click()
|
||||
time.sleep(3)
|
||||
|
||||
logger.info("구매 요청 완료! 구매 결과를 확인합니다...")
|
||||
|
||||
# 구매 결과 확인
|
||||
result = extract_purchase_result(driver)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"구매 중 오류 발생: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def extract_purchase_result(driver):
|
||||
"""구매 결과(번호) 추출"""
|
||||
result = {
|
||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"games": [],
|
||||
"status": "unknown",
|
||||
}
|
||||
|
||||
try:
|
||||
time.sleep(2)
|
||||
|
||||
# 회차 / 추첨일 추출
|
||||
try:
|
||||
result["round"] = driver.find_element(By.ID, "buyRound").text
|
||||
result["draw_date"] = driver.find_element(By.ID, "drawDate").text
|
||||
except:
|
||||
pass
|
||||
|
||||
# 번호 추출: #reportRow li div.nums span
|
||||
game_rows = driver.find_elements(By.CSS_SELECTOR, "#reportRow li")
|
||||
for row in game_rows:
|
||||
nums = row.find_elements(By.CSS_SELECTOR, "div.nums span")
|
||||
numbers = [int(n.text) for n in nums if n.text.strip().isdigit()]
|
||||
if numbers:
|
||||
result["games"].append(numbers)
|
||||
|
||||
if result["games"]:
|
||||
result["status"] = "success"
|
||||
logger.info(f"구매 성공! {len(result['games'])}게임")
|
||||
for i, game in enumerate(result["games"], 1):
|
||||
logger.info(f" 게임 {i}: {game}")
|
||||
else:
|
||||
result["status"] = "completed_but_numbers_unknown"
|
||||
logger.info("번호 추출 실패. 마이페이지에서 확인하세요.")
|
||||
|
||||
except Exception as e:
|
||||
result["status"] = "error"
|
||||
result["error"] = str(e)
|
||||
logger.warning(f"결과 추출 중 오류: {e}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def save_log(result):
|
||||
"""구매 기록 저장"""
|
||||
log_file = CONFIG["LOG_FILE"]
|
||||
history = []
|
||||
|
||||
if os.path.exists(log_file):
|
||||
try:
|
||||
with open(log_file, "r", encoding="utf-8") as f:
|
||||
history = json.load(f)
|
||||
except:
|
||||
history = []
|
||||
|
||||
history.append(result)
|
||||
|
||||
with open(log_file, "w", encoding="utf-8") as f:
|
||||
json.dump(history, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"구매 기록이 {log_file}에 저장되었습니다.")
|
||||
|
||||
|
||||
def main():
|
||||
"""메인 실행 함수"""
|
||||
logger.info("=" * 50)
|
||||
logger.info("로또 6/45 자동 구매 스크립트 시작")
|
||||
logger.info(f"실행 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
logger.info("=" * 50)
|
||||
|
||||
# 아이디/비밀번호 확인
|
||||
if CONFIG["USER_ID"] == "여기에_아이디_입력" or CONFIG["USER_PW"] == "여기에_비밀번호_입력":
|
||||
logger.error("아이디/비밀번호가 설정되지 않았습니다!")
|
||||
logger.error("환경변수(LOTTO_USER_ID, LOTTO_USER_PW)를 설정하거나 스크립트 내 CONFIG를 수정하세요.")
|
||||
sys.exit(1)
|
||||
|
||||
driver = None
|
||||
try:
|
||||
driver = create_driver()
|
||||
|
||||
# 1. 로그인
|
||||
if not login(driver):
|
||||
logger.error("로그인 실패로 종료합니다.")
|
||||
sys.exit(1)
|
||||
|
||||
# 2. 잔액 확인
|
||||
balance = check_balance(driver)
|
||||
cost = CONFIG["BUY_COUNT"] * 1000
|
||||
if balance >= 0 and balance < cost:
|
||||
logger.error(f"예치금 부족! 현재: {balance:,}원, 필요: {cost:,}원")
|
||||
logger.error("동행복권 사이트에서 예치금을 충전해주세요.")
|
||||
sys.exit(1)
|
||||
|
||||
# 3. 로또 구매
|
||||
result = buy_lotto(driver, CONFIG["BUY_COUNT"])
|
||||
|
||||
# 4. 결과 저장
|
||||
if result:
|
||||
save_log(result)
|
||||
else:
|
||||
save_log({
|
||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"status": "failed",
|
||||
"error": "구매 결과를 확인할 수 없습니다.",
|
||||
})
|
||||
|
||||
logger.info("스크립트 실행 완료!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"예상치 못한 오류: {e}")
|
||||
save_log({
|
||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
})
|
||||
|
||||
finally:
|
||||
if driver:
|
||||
driver.quit()
|
||||
logger.info("브라우저 종료")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
동행복권(dhlottery.co.kr) 로또 6/45 자동 구매 스크립트
|
||||
- 매주 금요일 자동 실행 (cron/스케줄러 연동)
|
||||
- 5게임 자동 구매
|
||||
- Selenium 기반 웹 자동화
|
||||
|
||||
사전 준비:
|
||||
pip install selenium
|
||||
Chrome 브라우저 + ChromeDriver 설치 필요
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
# .env 파일 자동 로드
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
except ImportError:
|
||||
pass
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 설정
|
||||
# ─────────────────────────────────────────────
|
||||
CONFIG = {
|
||||
"USER_ID": os.environ.get("LOTTO_USER_ID", "hyeonggil2"),
|
||||
"USER_PW": os.environ.get("LOTTO_USER_PW", "Rlaehdbs062$"),
|
||||
"BUY_COUNT": 1, # 구매 게임 수 (1~10)
|
||||
"HEADLESS": True, # True: 브라우저 창 숨김, False: 브라우저 창 표시
|
||||
"LOG_FILE": "logs/lotto_log.json",
|
||||
"SLACK_WEBHOOK_URL": os.environ.get("SLACK_WEBHOOK_URL", ""),
|
||||
}
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler("logs/lotto_auto.log", encoding="utf-8"),
|
||||
],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_driver():
|
||||
"""Chrome WebDriver 생성"""
|
||||
options = Options()
|
||||
if CONFIG["HEADLESS"]:
|
||||
options.add_argument("--headless=new")
|
||||
options.add_argument("--no-sandbox")
|
||||
options.add_argument("--disable-dev-shm-usage")
|
||||
options.add_argument("--disable-gpu")
|
||||
options.add_argument("--disable-software-rasterizer")
|
||||
options.add_argument("--remote-debugging-port=9222")
|
||||
options.add_argument("--user-data-dir=/tmp/chrome-user-data")
|
||||
options.add_argument("--window-size=1920,1080")
|
||||
# 자동화 탐지 우회
|
||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||
options.add_experimental_option("useAutomationExtension", False)
|
||||
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.execute_cdp_cmd(
|
||||
"Page.addScriptToEvaluateOnNewDocument",
|
||||
{"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"},
|
||||
)
|
||||
driver.set_page_load_timeout(30) # 페이지 로드 최대 30초
|
||||
driver.set_script_timeout(30) # JS 실행 최대 30초
|
||||
driver.implicitly_wait(5)
|
||||
return driver
|
||||
|
||||
|
||||
def login(driver):
|
||||
"""동행복권 사이트 로그인"""
|
||||
from selenium.common.exceptions import TimeoutException
|
||||
logger.info("로그인 시도 중...")
|
||||
try:
|
||||
driver.get("https://www.dhlottery.co.kr/login")
|
||||
except TimeoutException:
|
||||
logger.warning("로그인 페이지 로드 타임아웃 — 로드된 부분으로 계속 진행")
|
||||
time.sleep(2)
|
||||
|
||||
# 아이디/비밀번호 입력
|
||||
wait = WebDriverWait(driver, 10)
|
||||
id_input = wait.until(EC.element_to_be_clickable((By.ID, "inpUserId")))
|
||||
id_input.click()
|
||||
id_input.send_keys(CONFIG["USER_ID"])
|
||||
|
||||
pw_input = wait.until(EC.element_to_be_clickable((By.ID, "inpUserPswdEncn")))
|
||||
pw_input.click()
|
||||
pw_input.send_keys(CONFIG["USER_PW"])
|
||||
|
||||
# 로그인 버튼 클릭
|
||||
login_btn = wait.until(EC.element_to_be_clickable((By.ID, "btnLogin")))
|
||||
login_btn.click()
|
||||
|
||||
# /login이 URL에서 완전히 사라질 때까지 대기 (최대 15초)
|
||||
try:
|
||||
WebDriverWait(driver, 15).until(lambda d: "/login" not in d.current_url)
|
||||
except:
|
||||
pass
|
||||
|
||||
logger.info(f"로그인 후 URL: {driver.current_url}")
|
||||
|
||||
# 로그인 성공 확인 (URL이 /login에서 벗어나면 성공)
|
||||
if "/login" in driver.current_url:
|
||||
logger.error("로그인 실패! 아이디/비밀번호를 확인하세요.")
|
||||
return False
|
||||
|
||||
logger.info("로그인 성공!")
|
||||
return True
|
||||
|
||||
|
||||
def check_balance(driver):
|
||||
"""예치금 잔액 확인"""
|
||||
driver.get("https://www.dhlottery.co.kr/mypage/home")
|
||||
time.sleep(2)
|
||||
try:
|
||||
balance_elem = driver.find_element(By.CSS_SELECTOR, "#totalAmt")
|
||||
balance_text = balance_elem.text.replace(",", "").replace("원", "").strip()
|
||||
balance = int(balance_text) if balance_text.isdigit() else 0
|
||||
logger.info(f"현재 예치금 잔액: {balance:,}원")
|
||||
return balance
|
||||
except Exception as e:
|
||||
logger.warning(f"잔액 확인 실패: {e}")
|
||||
return -1
|
||||
|
||||
|
||||
def buy_lotto(driver, game_count=5):
|
||||
"""
|
||||
로또 6/45 자동 구매
|
||||
game_count: 구매할 게임 수 (1~10)
|
||||
"""
|
||||
logger.info(f"로또 {game_count}게임 자동 구매 시작...")
|
||||
|
||||
# 로또 구매 페이지 이동 (타임아웃 시 로드된 부분으로 계속 진행)
|
||||
from selenium.common.exceptions import TimeoutException
|
||||
try:
|
||||
driver.get("https://ol.dhlottery.co.kr/olotto/game/game645.do")
|
||||
except TimeoutException:
|
||||
logger.warning("페이지 로드 타임아웃 — 로드된 부분으로 계속 진행")
|
||||
time.sleep(2)
|
||||
|
||||
# iframe 전환 (로또 구매 페이지는 iframe 내부에 있음)
|
||||
try:
|
||||
wait = WebDriverWait(driver, 10)
|
||||
|
||||
# 모든 레이어 팝업을 JS로 직접 숨기기 (z-index 오버레이 완전 제거)
|
||||
driver.execute_script("""
|
||||
var popups = document.querySelectorAll(
|
||||
'#popupLayerAlert, #popupLayerConfirm, .layer-alert, .layer-popup, .pop_wrap'
|
||||
);
|
||||
popups.forEach(function(el) { el.style.display = 'none'; });
|
||||
""")
|
||||
time.sleep(0.5)
|
||||
logger.info("팝업 레이어 숨김 처리 완료")
|
||||
|
||||
# "자동" 탭 선택 — onclick 함수(selectWayTab)를 직접 호출
|
||||
driver.execute_script("selectWayTab(1);")
|
||||
time.sleep(1)
|
||||
time.sleep(1)
|
||||
|
||||
# 게임 수 설정 (드롭다운에서 선택)
|
||||
from selenium.webdriver.support.ui import Select
|
||||
try:
|
||||
select_elem = driver.find_element(By.ID, "amoundApply")
|
||||
select = Select(select_elem)
|
||||
select.select_by_value(str(game_count))
|
||||
time.sleep(0.5)
|
||||
except Exception:
|
||||
# 드롭다운이 없는 경우 자동번호 버튼을 game_count번 클릭
|
||||
for i in range(game_count):
|
||||
auto_select_btn = driver.find_element(By.ID, "btnSelectNum")
|
||||
auto_select_btn.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
# "자동번호 선택" 버튼 클릭
|
||||
try:
|
||||
select_num_btn = driver.find_element(By.ID, "btnSelectNum")
|
||||
select_num_btn.click()
|
||||
time.sleep(1)
|
||||
except:
|
||||
pass
|
||||
|
||||
# "구매하기" 버튼 클릭
|
||||
buy_btn = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, "btnBuy"))
|
||||
)
|
||||
buy_btn.click()
|
||||
time.sleep(2)
|
||||
|
||||
# 구매 확인 팝업 "확인" 클릭
|
||||
confirm_btn = wait.until(
|
||||
EC.element_to_be_clickable((By.CSS_SELECTOR, "#popupLayerConfirm input[value='확인']"))
|
||||
)
|
||||
confirm_btn.click()
|
||||
time.sleep(3)
|
||||
|
||||
logger.info("구매 요청 완료! 구매 결과를 확인합니다...")
|
||||
|
||||
# 구매 결과 확인
|
||||
result = extract_purchase_result(driver)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"구매 중 오류 발생: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def extract_purchase_result(driver):
|
||||
"""구매 결과(번호) 추출"""
|
||||
result = {
|
||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"games": [],
|
||||
"status": "unknown",
|
||||
}
|
||||
|
||||
try:
|
||||
time.sleep(2)
|
||||
|
||||
# 회차 / 추첨일 추출
|
||||
try:
|
||||
result["round"] = driver.find_element(By.ID, "buyRound").text
|
||||
result["draw_date"] = driver.find_element(By.ID, "drawDate").text
|
||||
except:
|
||||
pass
|
||||
|
||||
# 번호 추출: #reportRow li div.nums span
|
||||
game_rows = driver.find_elements(By.CSS_SELECTOR, "#reportRow li")
|
||||
for row in game_rows:
|
||||
nums = row.find_elements(By.CSS_SELECTOR, "div.nums span")
|
||||
numbers = [int(n.text) for n in nums if n.text.strip().isdigit()]
|
||||
if numbers:
|
||||
result["games"].append(numbers)
|
||||
|
||||
if result["games"]:
|
||||
result["status"] = "success"
|
||||
logger.info(f"구매 성공! {len(result['games'])}게임")
|
||||
for i, game in enumerate(result["games"], 1):
|
||||
logger.info(f" 게임 {i}: {game}")
|
||||
else:
|
||||
result["status"] = "completed_but_numbers_unknown"
|
||||
logger.info("번호 추출 실패. 마이페이지에서 확인하세요.")
|
||||
|
||||
except Exception as e:
|
||||
result["status"] = "error"
|
||||
result["error"] = str(e)
|
||||
logger.warning(f"결과 추출 중 오류: {e}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def send_slack(result):
|
||||
"""Slack Webhook으로 구매 결과 알림 전송"""
|
||||
url = CONFIG["SLACK_WEBHOOK_URL"]
|
||||
if not url:
|
||||
return
|
||||
|
||||
status = result.get("status", "unknown")
|
||||
games = result.get("games", [])
|
||||
round_no = result.get("round", "")
|
||||
draw_date = result.get("draw_date", "")
|
||||
|
||||
if status == "success" and games:
|
||||
games_text = "\n".join(
|
||||
f" 게임 {i}: {' '.join(str(n) for n in nums)}"
|
||||
for i, nums in enumerate(games, 1)
|
||||
)
|
||||
text = (
|
||||
f":ticket: *로또 구매 완료!*\n"
|
||||
f"회차: {round_no} 추첨일: {draw_date}\n"
|
||||
f"{games_text}"
|
||||
)
|
||||
elif status == "failed":
|
||||
text = f":warning: *로또 구매 실패*\n{result.get('error', '')}"
|
||||
else:
|
||||
text = f":x: *로또 구매 오류*\n{result.get('error', status)}"
|
||||
|
||||
try:
|
||||
payload = json.dumps({"text": text}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
logger.info("Slack 알림 전송 완료")
|
||||
except Exception as e:
|
||||
logger.warning(f"Slack 알림 전송 실패: {e}")
|
||||
|
||||
|
||||
def save_log(result):
|
||||
"""구매 기록 저장"""
|
||||
log_file = CONFIG["LOG_FILE"]
|
||||
history = []
|
||||
|
||||
if os.path.exists(log_file):
|
||||
try:
|
||||
with open(log_file, "r", encoding="utf-8") as f:
|
||||
history = json.load(f)
|
||||
except:
|
||||
history = []
|
||||
|
||||
history.append(result)
|
||||
|
||||
with open(log_file, "w", encoding="utf-8") as f:
|
||||
json.dump(history, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"구매 기록이 {log_file}에 저장되었습니다.")
|
||||
|
||||
|
||||
def main():
|
||||
"""메인 실행 함수"""
|
||||
logger.info("=" * 50)
|
||||
logger.info("로또 6/45 자동 구매 스크립트 시작")
|
||||
logger.info(f"실행 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
logger.info("=" * 50)
|
||||
|
||||
# 아이디/비밀번호 확인
|
||||
if CONFIG["USER_ID"] == "여기에_아이디_입력" or CONFIG["USER_PW"] == "여기에_비밀번호_입력":
|
||||
logger.error("아이디/비밀번호가 설정되지 않았습니다!")
|
||||
logger.error("환경변수(LOTTO_USER_ID, LOTTO_USER_PW)를 설정하거나 스크립트 내 CONFIG를 수정하세요.")
|
||||
sys.exit(1)
|
||||
|
||||
driver = None
|
||||
try:
|
||||
driver = create_driver()
|
||||
|
||||
# 1. 로그인
|
||||
if not login(driver):
|
||||
logger.error("로그인 실패로 종료합니다.")
|
||||
sys.exit(1)
|
||||
|
||||
# 2. 잔액 확인
|
||||
balance = check_balance(driver)
|
||||
cost = CONFIG["BUY_COUNT"] * 1000
|
||||
if balance >= 0 and balance < cost:
|
||||
logger.error(f"예치금 부족! 현재: {balance:,}원, 필요: {cost:,}원")
|
||||
logger.error("동행복권 사이트에서 예치금을 충전해주세요.")
|
||||
sys.exit(1)
|
||||
|
||||
# 3. 로또 구매
|
||||
result = buy_lotto(driver, CONFIG["BUY_COUNT"])
|
||||
|
||||
# 4. 결과 저장 및 Slack 알림
|
||||
if result:
|
||||
save_log(result)
|
||||
send_slack(result)
|
||||
else:
|
||||
failed = {
|
||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"status": "failed",
|
||||
"error": "구매 결과를 확인할 수 없습니다.",
|
||||
}
|
||||
save_log(failed)
|
||||
send_slack(failed)
|
||||
|
||||
logger.info("스크립트 실행 완료!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"예상치 못한 오류: {e}")
|
||||
error_result = {
|
||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
}
|
||||
save_log(error_result)
|
||||
send_slack(error_result)
|
||||
|
||||
finally:
|
||||
if driver:
|
||||
driver.quit()
|
||||
logger.info("브라우저 종료")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
lotto-runner/requirements.txt
Normal file
2
lotto-runner/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
selenium
|
||||
python-dotenv
|
||||
Reference in New Issue
Block a user