더베이스 예약취소 감시

더베이스

풋살을 즐기고 싶었지만 더베이스는 정말 예약이 빡세다.
그래서 예약 매크로를 만들어볼까 싶어서 시작한 매크로 개발기이다.

더이상 시간을 쓰고싶진 않아서 예약매크로는 실패했지만, 예약취소를 감시해서 텔레그램 알람을 받는 매크로까지는 성공했고 이걸 만들면서 텔레그램 봇도 만들고 재밌었기 때문에 정리해본다.

개발자도구 방지 우회

매크로를 만들려면 자바스크립트가 어떻게 동작하는질 확인해야한다.
코드 분석을 위해 개발자도구를 켰는데, Ctrl+Shift+I, F12 를 눌러봐도 개발자 도구가 켜지지 않고, 우클릭도 되지 않았었다.

방법을 찾던 끝에 페이지 진입 전에 개발자도구를 켜놓고 들어가면 될 것 같아서 시도해봤는데, 개발자 도구가 켜져있는 것을 감지하고 페이지가 종료되거나 이전 페이지로 이동 돼버렸다.

아마 자바스크립트가 페이지 로드와 동시에 자동으로 실행되면서 개발자도구를 감지하고 페이지를 종료시킨 것 같았다.

개발자도구를 켠 후 더베이스 페이지에 진입한 다음 빠르게 스레드를 일시정지해서 페이지 종료 코드가 실행되기 전에 멈춰뒀더니 통과됐다.
이때 자바스크립트 코드가 처음부터 있던게 아니라 동적으로 다운받아서 실행되기 때문에 너무 빠르게 멈추면 안된다.

bc27a827-521a-4f48-88f8-80af784f7988
bc27a827-521a-4f48-88f8-80af784f7988

개발자도구에서 Ctrl+Shift+Fdevtool을 검색하면 어떤 v라는 함수 하나가 나온다. 그 함수가 선언된 파일을 우클릭해서 override content 하면 권한을 허용하겠냐는 메시지가 뜨고, 허용하게되면 폴더를 선택하게 되고 파일 수정이 가능하도록 변한다.

일단 나는 한번 했기 때문에 “DEVTOOL DETECT” 이후 즉시 리턴하도록 수정까지 되어있는 스크린샷이다.

만약 잘못해서 허용을 안했거나 파일시스템 권한이 없다는 문제가 발생하면 브라우저를 다시켜서 한번 더 해보면 되고, 그래도 안된다면 Settings > Persistence > Enable Local Overrides 이걸 설정을 해보면 될것이다.

230950d7-bda0-41d5-a109-239dd6385c54
230950d7-bda0-41d5-a109-239dd6385c54

예약취소 감시 봇

구조를 살펴보니 날짜를 누르면 예약 가능한 시간대가 표시되는 구조였고 패킷을 확인해보니 날짜(date)와 구장(stadium_code)를 담아 getPriceStadiumTotalList 요청을 보내면 응답에 예약 가능한 시간대를 주는 것을 볼 수 있었다.

a74e47ab-a53e-4815-a398-be096c44410e
a74e47ab-a53e-4815-a398-be096c44410e

2025-07-08 의 04:00 ~ 06:00 시간대는 예약 가능하다.

bd3fe2f8-62b8-4949-af25-6c1bb00e6dc4
bd3fe2f8-62b8-4949-af25-6c1bb00e6dc4

더 분석할 시간이 없어서 이걸로 매크로를 만들어봤다.

구조는 그냥 요청을 보내고 응답을 받아서 텔레그램 봇으로 알림을 주는 방식이다.

import requests
import json
import time


# 0. 텔레그램에서 thebaseadmin_bot 을 검색 후 start (t.me/thebaseadmin_bot)
# 1. 메시지를 받을 텔레그램 계정의 CHAT_ID 로 업데이트 필요
# 아래 조건문 True로 변경 후 콘솔에 출력된 값 확인  
# ex) 'chat': {'id': 1311276840 << 여기에 있음
if False:
    url = f'https://api.telegram.org/bot{TOKEN}/getUpdates'
    response = requests.get(url)
    print(response.json())

    exit()

######### 3. 원하는 시간대 및 기본값 세팅 ###########
stardium_list = ["ST_0_IN", "ST_1", "ST_2",] # "ST_3", "ST_4", "ST_5", "ST_6", "ST_7"]
date_list = ["2025-06-19", "2025-06-26"]
time_list = {"18:00 ~ 20:00", "19:00 ~ 21:00"}


# http 요청 인터벌
QUERY_INTERVAL = 1
# 정보 업데이트 인터벌 (분)
UPDATE_INTERVAL = 1
# 이용 가능한 구장이 있는 경우에만 메시지 전송 (True / False)
ONLY_AVAILABLE = False


# 토큰은 고정값
BOT_TOKEN = '7686351927:AAFc8ctALyPaft8giGGII1jheJMcwflPlGA'
# chat_id 는 위의 1. 에서 조건문을 True로 변경 후 나온 결과값에서 확인
CHAT_ID = 1311276840
########################################

STARDIUM_MAP = {
    'ST_0_IN': '실내구장(9F)',
    'ST_1': '1구장(8F)',
    'ST_2': '2구장(8F)',
    'ST_3': '3구장(8F)',
    'ST_4': '4구장(8F)',
    'ST_5': '5구장(8F)',
    'ST_6': '6구장(10F)',
    'ST_7': '7구장(10F)',
}

def get_reservation_status(interval = 2):
    global stardium_list, date_list, time_list
    # 요청보낼 URL
    url = "https://www.futsalbase.com/api/price/getPriceStadiumTotalList"

    # 헤더 설정 (User-Agent로 실제 브라우저인척)
    headers = {
        "Content-Type": "application/json",
        "Origin": "https://www.futsalbase.com",
        "Referer": "https://www.futsalbase.com/home",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
    }

    all_results = []
    for s in stardium_list:
        for x in date_list:
            # 보낼 데이터
            data = {
                "date": x,
                "stadium_code": s
            }

            # POST 요청 보내기
            response = requests.post(url, json=data, headers=headers)
            print(response.status_code)
            response = response.text
            # print(response)
            data = json.loads(response)
            results = [
                {
                    "stardium": s,
                    "date": item["date"],
                    "time": item["time"],
                    "is_open": item["is_open"],
                    "is_reserved": item["is_reserved"]
                }
                for item in data["result"]
                if item["time"] in time_list
            ]
            all_results.append(results)
            time.sleep(interval)
    return all_results

# all_results = get_reservation_status(1)
# for rs in all_results:
#     for r in rs:
#         print(r)


def send_telegram_message(token, chat_id, message):
    url = f"https://api.telegram.org/bot{token}/sendMessage"
    payload = {
        "chat_id": chat_id,
        "text": message
    }
    requests.post(url, data=payload)

from collections import defaultdict
from datetime import datetime
def generate_message(all_results, only_avail = False):
    # 날짜별로 모으기
    date_grouped = defaultdict(list)

    for entry_list in all_results:
        for entry in entry_list:
            date = entry['date']
            stadium = entry['stardium']
            time = entry['time']
            reserved = entry['is_reserved']
            date_grouped[date].append((stadium, time, reserved))

    # 메시지 포맷 만들기
    header = []
    now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S KST")
    header.append(f"🕒 업데이트 시간 {now_str}")
    header.append("")

    all_lines = []
    for date in sorted(date_grouped.keys()):
        lines = []
        for stadium, time, reserved in date_grouped[date]:
            if only_avail and reserved == 1:
                continue
            lines.append(f"{"❌" if reserved == 1 else "✅"} {STARDIUM_MAP[stadium]}{time} ({"예약불가" if reserved == 1 else "예약가능"})")

        # 데이터가 있는 경우만
        if len(lines):
            lines.insert(0, f"📅 {date}")
            lines.append("")
        all_lines += lines

    if only_avail and len(all_lines) == 0:
        return None
    return "\n".join(header + all_lines).strip()

send_telegram_message(BOT_TOKEN, CHAT_ID, "🚨 테스트중입니다!")

while True:
    msg = generate_message(get_reservation_status(QUERY_INTERVAL), ONLY_AVAILABLE)
    if msg is not None:
        send_telegram_message(BOT_TOKEN, CHAT_ID, msg)
    else:
        print("이용 가능한 구장이 없습니다!")
    time.sleep(UPDATE_INTERVAL * 60)

결과확인

5a8e12d8-d97e-454c-8e09-b60414980678
5a8e12d8-d97e-454c-8e09-b60414980678

ESC
Type to search...