LFU : Last Fotress Underground – 라스트포트리스 홈페이지 출석 체크 로그인 하기 귀찮아서 제작

패키지 설치

  • Flask : v2.0.3
  • selenium : v4.0.0
  • PyQt5 : v5.15.6
  • PyQtWebEngine : v5.15.5

html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>출석체크 매크로</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
    <div class="container">
        <h1>출석체크 매크로</h1>
        <div class="form-group">
            <label for="account_id">계정 ID:</label>
            <input type="text" id="account_id" placeholder="계정 ID 입력">
        </div>
        <div class="form-group">
            <label for="saved_ids">저장된 ID:</label>
            <select id="saved_ids">
                <option value="">저장된 ID 선택</option>
            </select>
        </div>
        <div class="button-group">
            <button id="save_id_btn">ID 저장</button>
            <button id="check_in_btn">출석체크</button>
        </div>
        <pre id="output"></pre>
    </div>

    <script>
        $(document).ready(function() {
            function loadSavedIds() {
                var savedIds = JSON.parse(localStorage.getItem('savedIds') || '[]');
                var select = $('#saved_ids');
                select.empty();
                select.append('<option value="">저장된 ID 선택</option>');
                savedIds.forEach(function(id) {
                    select.append('<option value="' + id + '">' + id + '</option>');
                });
            }

            loadSavedIds();

            $('#save_id_btn').click(function() {
                var accountId = $('#account_id').val();
                if (accountId) {
                    var savedIds = JSON.parse(localStorage.getItem('savedIds') || '[]');
                    if (!savedIds.includes(accountId)) {
                        savedIds.push(accountId);
                        localStorage.setItem('savedIds', JSON.stringify(savedIds));
                        loadSavedIds();
                    }
                } else {
                    alert("계정 ID를 입력해주세요.");
                }
            });

            $('#saved_ids').change(function() {
                var selectedId = $(this).val();
                if (selectedId) {
                    $('#account_id').val(selectedId);
                }
            });

            $('#check_in_btn').click(function() {
                var accountId = $('#account_id').val();
                if (accountId) {
                    $.ajax({
                        url: '/run_automation',
                        type: 'POST',
                        contentType: 'application/json',
                        data: JSON.stringify({ account_id: accountId }),
                        success: function(response) {
                            $('#output').text(response.output || '응답 오류: 데이터가 없습니다.');
                        },
                        error: function(error) {
                            $('#output').text('오류 발생: ' + error.responseText);
                        }
                    });
                } else {
                    alert("계정 ID를 입력해주세요.");
                }
            });

            // 실시간 로그 스트리밍
            const eventSource = new EventSource('/logs');
            eventSource.onmessage = function (e) {
                const log = document.getElementById('output');
                log.textContent += e.data + "\n";
                log.scrollTop = log.scrollHeight;
            };
        });
    </script>
</body>
</html>

styles.css

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: Arial, sans-serif;
    background-color: #f4f4f4;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

.container {
    background: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    width: 90%;
    max-width: 500px;
}

h1 {
    margin-bottom: 20px;
    font-size: 24px;
    text-align: center;
}

.form-group {
    margin-bottom: 15px;
    display: flex;
    align-items: center;
}

label {
    flex: 1;
    margin-right: 10px;
    font-weight: bold;
}

input[type="text"], select {
    flex: 2;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 16px;
}

.button-group {
    display: flex;
    justify-content: space-between;
    margin-bottom: 15px;
}

.button-group button {
    width: 48%;
    padding: 10px;
    background: #007BFF;
    border: none;
    border-radius: 4px;
    color: white;
    font-size: 16px;
    cursor: pointer;
}

.button-group button:hover {
    background: #0056b3;
}

pre {
    margin-top: 20px;
    background: #f4f4f4;
    padding: 10px;
    border-radius: 4px;
    white-space: pre-wrap;
    word-wrap: break-word;
}

@media (max-width: 600px) {
    h1 {
        font-size: 20px;
    }
    
    input[type="text"], select, button {
        font-size: 14px;
        padding: 8px;
    }
    
    .button-group {
        flex-direction: column;
        gap: 10px;
    }

    .button-group button {
        width: 100%;
    }
}

py

from flask import Flask, request, jsonify, render_template, Response
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import os
import logging
from io import StringIO
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import QUrl

app = Flask(__name__)

# Logging 설정
log_stream = StringIO()
logging.basicConfig(stream=log_stream, level=logging.INFO, format='%(asctime)s - %(message)s')

@app.route('/')
def index():
    return render_template('test3.html')

def generate_log():
    while True:
        log_content = log_stream.getvalue()
        if log_content:
            yield f"data: {log_content}\n\n"
            log_stream.truncate(0)
            log_stream.seek(0)

@app.route('/logs')
def stream_logs():
    return Response(generate_log(), mimetype='text/event-stream')

@app.route('/run_automation', methods=['POST'])
def run_automation():
    data = request.get_json()
    account_id = data['account_id']
    driver_path = data.get('driver_path', 'C:/dev/seleniumdrivers/chromedriver.exe')

    service = Service(driver_path)
    options = webdriver.ChromeOptions()
    options.add_argument('--disable-extensions')
    options.add_argument('--disable-infobars')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-gpu')
    options.add_argument('--window-size=1920x1080')
    options.add_argument('--headless')
    options.add_argument('--blink-settings=imagesEnabled=false')

    try:
        driver = webdriver.Chrome(service=service, options=options)
        logging.info("웹사이트 열기 완료")
        driver.get("https://lf-vip.store.koppay.net/integral/ko")
        logging.info("로그인 페이지 로드 완료")

        login_button = WebDriverWait(driver, 10).until(
            EC.element_to_be_clickable((By.CSS_SELECTOR, ".no_login_btn"))
        )
        login_button.click()
        logging.info("로그인 버튼 클릭 완료")

        account_id_field = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".site-input__inner"))
        )
        account_id_field.clear()
        account_id_field.send_keys(account_id)
        logging.info(f"계정 ID 입력 완료: {account_id}")

        green_button = WebDriverWait(driver, 10).until(
            EC.element_to_be_clickable((By.CSS_SELECTOR, ".check_btn._flex"))
        )
        green_button.click()
        logging.info("초록색 버튼 클릭 완료")

        confirm_button = WebDriverWait(driver, 10).until(
            EC.element_to_be_clickable((By.CSS_SELECTOR, ".cfm_btn"))
        )
        confirm_button.click()
        logging.info("확인 버튼 클릭 완료")

        user_info = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".user_info"))
        )
        logging.info("로그인 성공")

        try:
            close_icon = WebDriverWait(driver, 10).until(
                EC.element_to_be_clickable((By.CSS_SELECTOR, ".close_icon"))
            )
            close_icon.click()
            logging.info("팝업창 닫기 완료")
        except Exception as e:
            logging.error(f"팝업창 닫기 실패: {str(e)}")

        try:
            sign_in_buttons = WebDriverWait(driver, 10).until(
                EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".day_btn"))
            )
            logging.info(f"수령 버튼 수: {len(sign_in_buttons)}")
            button_clicked = False
            for button in sign_in_buttons:
                button_text = button.text
                if "수령" in button_text:
                    try:
                        button.click()
                        logging.info("수령 버튼 클릭 완료")
                        button_clicked = True
                        break
                    except Exception as click_exception:
                        logging.error(f"수령 버튼 클릭 오류: {str(click_exception)}")

            if not button_clicked:
                logging.info("수령 버튼 누를거 없음")
        except Exception as e:
            logging.error(f"수령 버튼을 찾는 중 오류 발생: {str(e)}")

        driver.quit()
    except Exception as e:
        logging.error(f"오류 발생: {str(e)}")
        driver.quit()

    return jsonify({"output": "작업 완료"})

if __name__ == '__main__':
    # Flask 앱을 백그라운드에서 실행
    from threading import Thread
    server_thread = Thread(target=lambda: app.run(debug=False, host='127.0.0.1', port=5000))
    server_thread.start()

    # PyQt5 애플리케이션
    app = QApplication(sys.argv)
    browser = QWebEngineView()
    browser.setWindowTitle("LFU Automation")
    browser.resize(1280, 720)
    browser.setUrl(QUrl("https://127.0.0.1:5000"))
    browser.show()
    sys.exit(app.exec_())

chromedriver 다운 페이지

chromdriver를 설치해서 경로 지정해줘야 함

실행 방식

  • 웹페이지 뜨지 않고 헤드리스 방식
  • 실시간 로그 확인 방식
  • 오류 발생하는 이유
    • 많은 계정 로그인 시켜서 봇 방지 팝업창 때문에 오류
    • 수령 버튼이 활성화 되어 있지 않는데 7개 수령 버튼을 눌렀을시 오류 로그 출력
  • 봇 방지 우회는 넣지 않음
  • 출석체크 상자 수령하는 방식은 ‘수령’ 버튼이 활성화 되어있는것을 누르는 방식이 아닌 7개 그냥 다 누르는 방식(무식한 방식)

실행 화면 모습

기존에는 웹페이지를 띄운 다음에 사용했어야 하지만 실행파일을 실행하면 창이 뜨도록 설정

미구현 및 구현해야할것

  • 존재하지 않는 계정 ID 입력시 프로그램은 그냥 정상 작동
    • 확인 버튼이 활성화 되지 않을시 프로그램 종료 및 로그 출력
  • 봇 우회
  • 로딩 속도 개선

Categorized in:

Computer,