ktx 자동 예매,, 내가 만들어서 쓴다.

동동·2024년 4월 23일
56
post-thumbnail

2024년 6월, 코레일 홈페이지가 대대적으로 리뉴얼되면서 기존 자동 예매 코드가 더 이상 동작하지 않게 되었습니다.
이 글에서 소개했던 방식이 막히면서, 새롭게 구조를 뜯어고치고, 최신 Selenium과 Spring Boot로 다시 구현했습니다.
코레일 홈페이지 리뉴얼에 대응하기


서울에 살면서 지방에 내려갈 때마다 좌석이 매진되는 일이 종종 있는데 그래서 크롬에서 지원되는 KTX 매크로를 찾아보았는데, 이게 지원이 종료된 건지… 안된다.

그래서 내가 만들어서 쓰자.. 라고 생각이 들어 바로 실행에 옮겨보았습니다 ! (불법적인 목적 ❌)

Selenium을 사용해서 간단하게 KTX 자동 예매 프로그램을 만들었습니다.


로컬에서 서버 실행 후 /swagger-ui 로 이동하셔서 값들 넣어서 실행해주시면 됩니다!

깃허브 링크


KTX 자동 예매

Specifications.

  • [필수 기능] 코레일 웹사이트에 로그인할 수 있는 기능이 있어야 합니다.
  • [필수 기능] 기차표 예약 페이지로 이동하려면 이동하려는 버튼을 클릭해야 합니다.
  • [필수 기능] 적절한 날짜, 시작 및 종료 역, 원하는 열차의 시간 범위를 선택해야 합니다.
  • [필수 기능] 지정된 시간 내에 잔여 티켓이 있는지 지속적으로 확인하고 좌석 예약을 시도해야 합니다.
  • [추가 기능] GUI를 사용할 수 없는 경우, 헤드리스 모드에서도 작동해야 합니다.

Layers.

  • 컨트롤러 레이어(TicketController): 사용자 상호 작용을 위한 REST API 엔드포인트 제공
    • @GetMapping("/ticket/reserve"): 이 엔드포인트은 티켓 예약 프로세스를 트리거합니다. 사용자 로그인 정보, 열차 정보, 시간과 같은 매개변수를 허용합니다.
  • 서비스 레이어(TicketService): Korail 웹사이트와 상호작용하기 위한 브라우저 자동화를 포함한 비즈니스 로직을 처리
    • initializeWebDriver(): 해당 메서드는 WebDriver를 설정합니다.
      WebDriver는 Chrome, Safari(주석처리, 헤드리스 모드 지원 안됨)를 사용할 수 있도록 하였습니다.
      현재 코드로는 GUI를 사용할 수 없는 배포 환경에서 실행 가능한 헤드리스 모드를 사용하도록 되어 있습니다.
    • loginUser(): 코레일 웹사이트 로그인 과정을 자동화합니다.
      명시적인 대기를 사용하여 요소와 상호 작용하기 전에 요소가 존재하고 클릭 가능한지 확인하므로 느린 페이지 로딩으로 인한 오류를 방지하는 데 도움이 됩니다.
    • navigateToReservationPage()checkAndReserveTicket(): 해당 메소드는 예약 페이지로 이동한 다음 지정된 시간 내에 사용 가능한 티켓을 지속적으로 확인합니다.
      기준에 맞는 티켓이 발견되면 예약을 시도합니다.
    • setTicketSearchCriteria(), findAndReserveAvailableTicket()handleReservationModal(): 검색 매개변수 설정, 예약 가능한 티켓 찾기 및 KTX-산천의 경우, 예약 시 모달 처리를 통해 예약 프로세스를 지원합니다.
  • 구성 계층(SwaggerConfig): API 문서를 자동으로 생성하도록 Swagger를 구성합니다.

Libraries.

  • Selenium WebDriver: 코레일 예약 시스템과 상호 작용하기 위한 브라우저 작업을 자동화
  • Swagger(OpenAPI): API 문서 생성용

application.properties 세팅.

application.properties

springdoc.swagger-ui.path = /swagger
springdoc.swagger-ui.tags-sorter=alpha
springdoc.swagger-ui.operations-sorter=alpha
springdoc.api-docs.path=/api-docs

springdoc.default-consumes-media-type=application/json;charset=UTF-8
springdoc.default-produces-media-type=applicionation/json;charset=UTF-8


추가적인 설명 (with 코드.)

// Web driver initialize
    private WebDriver initializeWebDriver() {
        // Chrome Driver
        WebDriverManager.chromedriver().setup();

        // Chrome option : headless
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless");

        WebDriver driver = new ChromeDriver(options);

        // Safari Drvier
        // System.setProperty("webdriver.safari.driver", "/System/Cryptexes/App/usr/bin/safaridriver");
        // WebDriver driver = new SafariDriver();
        driver.manage().window().setSize(new Dimension(1600, 1200));
        return driver;
    }

initializeWebDriver()

웹 드라이버는 Chrome, Safari Driver를 사용하였습니다.

💡 Safari Driver의 경우, 직접 설치하여 실행하여야 합니다. (Safari의 경우 headless 모드를 지원하지 않습니다.)

💡 headless 모드 사용을 하지 않을 경우는 옵션부분을 지우면 됩니다.

💡 Selenium 4.0 버전 이상부터는 WebDriverManager를 사용하면 드라이버가 저절로 실행됩니다.


private WebElement findReservationButton(WebDriver driver, int index) {
        try {
            WebElement regularSeatElement = driver.findElement(
                    By.xpath("//*[@id='tableResult']/tbody/tr[" + index + "]/td[6]"));
            WebElement specialSeatElement = driver.findElement(
                    By.xpath("//*[@id='tableResult']/tbody/tr[" + index + "]/td[5]"));

            WebElement regularSeatImg = regularSeatElement.findElement(By.tagName("img"));
            WebElement specialSeatImg = specialSeatElement.findElement(By.tagName("img"));

            String altRegular = regularSeatImg.getAttribute("alt");
            String altSpecial = specialSeatImg.getAttribute("alt");

            if (altRegular.equals("예약하기") && altSpecial.equals("예약하기")) {
                return regularSeatElement.findElement(By.tagName("a"));
            } else if (altRegular.equals("예약하기")) {
                return regularSeatElement.findElement(By.tagName("a"));
            } else if (altSpecial.equals("예약하기")) {
                return specialSeatElement.findElement(By.tagName("a"));
            } else {
                return null;
            }
        } catch (NoSuchElementException e) {
            return null;
        }
    }

findReservationButton()

좌석 예매의 경우엔, 일반석 예매를 우선으로 하였습니다.

💡 (입석+좌석)는 예매가 안됩니다.

💡 예매 과정에서 좌석 선택은 수동으로 하여야하기 때문에, 자동으로 좌석이 선택됩니다.


private void handleReservationModal(WebDriver driver, WebDriverWait wait) {
        WebElement modal = driver.findElement(By.id("korail-modal-traininfo"));
        if (modal.isDisplayed()) {
            WebElement iframe = modal.findElement(By.tagName("iframe"));
            driver.switchTo().frame(iframe);
            WebElement continueButton = wait.until(
                    ExpectedConditions.elementToBeClickable(By.cssSelector(".cont p.btn_c a")));
            continueButton.click();
            driver.switchTo().defaultContent();
        }
    }

handleReservationModal()

기존의 KTX의 경우엔 모달창이 따로 뜨진 않지만, KTX -산천의 경우엔 모달창이 뜹니다.

모달창의 확인 버튼을 누르지 않으면 예약이 되지 않기 때문에, 해당 로직을 추가로 넣어주었습니다 !



⚠️  해당 프로젝트는 개인적인 용도로만 사용하여 주시고, 불법적인 거래나 행위는 엄격히 금지됩니다.
      본 프로젝트 사용 시 관련 법률, 규정 및 서비스 약관을 준수하는 것은 사용자의 책임입니다.
      본 프로젝트의 사용으로 인한 어떠한 문제에 대해서도 본 프로젝트 개발자는 책임을 지지 않습니다.

profile
기술 블로그 → donghyeun02.com

6개의 댓글

comment-user-thumbnail
2024년 5월 2일

이 글을 읽고 셀레니움 크롤링을 공부하는 계기가 되었습니다 감사합니다

1개의 답글
comment-user-thumbnail
2024년 12월 26일

안녕하세요ㅎㅎ
명절 같은 지정된 시간대에 신청하는 경우에는 어떻게 하시는지 궁금합니다~~!
배치를 돌려서 진행하시나요?

1개의 답글
comment-user-thumbnail
2025년 4월 20일

안녕하세요.
코레일 사이트 개편후
일반 크롬이나 엣지로는 코레일 로그인 잘 되는데
셀레니움 크롬이나 엣지드라이버로 손으로 로그인 하면 통신 오류 뜨네요

이전 사이트는 잘 되었는데
도대체 뭘로 체크하면서 막은거지..
짐작가시는게 있나요

1개의 답글